HttpClient.GetAsync (Hoài) không bao giờ quay lại khi sử dụng await / async


315

Chỉnh sửa: Câu hỏi này có vẻ như có thể là cùng một vấn đề, nhưng không có câu trả lời ...

Chỉnh sửa: Trong trường hợp thử nghiệm 5, tác vụ dường như bị kẹt trong WaitingForActivationtrạng thái.

Tôi đã gặp một số hành vi kỳ quặc khi sử dụng System.Net.Http.HttpClient trong .NET 4.5 - trong đó "chờ" kết quả của một cuộc gọi đến (ví dụ) httpClient.GetAsync(...)sẽ không bao giờ quay lại.

Điều này chỉ xảy ra trong một số trường hợp khi sử dụng chức năng ngôn ngữ async / await mới và API Nhiệm vụ - mã dường như luôn hoạt động khi chỉ sử dụng các phần tiếp theo.

Đây là một số mã tái tạo vấn đề - thả mã này vào một "dự án WebApi MVC 4" mới trong Visual Studio 11 để hiển thị các điểm cuối GET sau:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

Mỗi điểm cuối ở đây trả về cùng một dữ liệu (các tiêu đề phản hồi từ stackoverflow.com) ngoại trừ /api/test5không bao giờ hoàn thành.

Tôi đã gặp phải một lỗi trong lớp HttpClient hay tôi đang lạm dụng API theo một cách nào đó?

Mã để sao chép:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}

2
Có vẻ như đây không phải là vấn đề tương tự, nhưng chỉ để đảm bảo bạn biết về nó, có một lỗi MVC4 trong các phương thức đồng bộ hóa WRT beta hoàn thành đồng bộ - xem stackoverflow.com/questions/9627329/
James Manning

Cảm ơn - Tôi sẽ coi chừng đó. Trong trường hợp này tôi nghĩ rằng phương thức phải luôn luôn không đồng bộ vì lệnh gọi đến HttpClient.GetAsync(...)?
Benjamin Fox

Câu trả lời:


468

Bạn đang sử dụng sai API.

Đây là tình huống: trong ASP.NET, chỉ có một luồng có thể xử lý một yêu cầu tại một thời điểm. Bạn có thể thực hiện một số xử lý song song nếu cần thiết (mượn các luồng bổ sung từ nhóm luồng), nhưng chỉ một luồng sẽ có bối cảnh yêu cầu (các luồng bổ sung không có ngữ cảnh yêu cầu).

Điều này được quản lý bởi ASP.NETSynchronizationContext .

Theo mặc định, khi bạn awaita Task, phương thức sẽ tiếp tục trên một bản chụp SynchronizationContext(hoặc một bản chụp TaskScheduler, nếu không có SynchronizationContext). Thông thường, đây chỉ là những gì bạn muốn: một hành động của bộ điều khiển không đồng bộ sẽ có awaitthứ gì đó, và khi nó tiếp tục, nó sẽ tiếp tục với bối cảnh yêu cầu.

Vì vậy, đây là lý do tại sao test5thất bại:

  • Test5Controller.Getthực thi AsyncAwait_GetSomeDataAsync(trong bối cảnh yêu cầu ASP.NET).
  • AsyncAwait_GetSomeDataAsyncthực thi HttpClient.GetAsync(trong bối cảnh yêu cầu ASP.NET).
  • Yêu cầu HTTP được gửi đi và HttpClient.GetAsynctrả về chưa hoàn thành Task.
  • AsyncAwait_GetSomeDataAsyncchờ đợi Task; vì nó không hoàn thành, AsyncAwait_GetSomeDataAsynctrả về một chưa hoàn thành Task.
  • Test5Controller.Get chặn luồng hiện tại cho đến khi Taskhoàn thành.
  • Phản hồi HTTP xuất hiện và hoàn Tasktrả bởi HttpClient.GetAsynchoàn thành.
  • AsyncAwait_GetSomeDataAsynccố gắng tiếp tục trong bối cảnh yêu cầu ASP.NET. Tuy nhiên, đã có một luồng trong ngữ cảnh đó: luồng bị chặn Test5Controller.Get.
  • Bế tắc.

Đây là lý do tại sao những người khác làm việc:

  • ( test1, test2test3): Continuations_GetSomeDataAsynclên lịch tiếp tục cho nhóm luồng, bên ngoài bối cảnh yêu cầu ASP.NET. Điều này cho phép Tasktrả về Continuations_GetSomeDataAsyncđể hoàn thành mà không cần phải nhập lại bối cảnh yêu cầu.
  • ( test4test6): Vì Taskđược chờ đợi , luồng yêu cầu ASP.NET không bị chặn. Điều này cho phép AsyncAwait_GetSomeDataAsyncsử dụng bối cảnh yêu cầu ASP.NET khi nó sẵn sàng tiếp tục.

Và đây là cách thực hành tốt nhất:

  1. Trong asyncphương pháp "thư viện" của bạn , sử dụng ConfigureAwait(false)bất cứ khi nào có thể. Trong trường hợp của bạn, điều này sẽ thay đổi AsyncAwait_GetSomeDataAsyncthànhvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Đừng chặn trên Tasks; đó là asynctất cả các con đường xuống. Nói cách khác, sử dụng awaitthay vì GetResult( Task.ResultTask.Waitcũng nên được thay thế bằng await).

Bằng cách đó, bạn nhận được cả hai lợi ích: phần tiếp theo (phần còn lại của AsyncAwait_GetSomeDataAsyncphương thức) được chạy trên một chuỗi nhóm luồng cơ bản không phải nhập vào ngữ cảnh yêu cầu ASP.NET; và chính bộ điều khiển là async(không chặn luồng yêu cầu).

Thêm thông tin:

Cập nhật 2012/07/13: Kết hợp câu trả lời này vào một bài đăng trên blog .


2
Có một số tài liệu cho ASP.NET SynchroniztaionContextgiải thích rằng có thể chỉ có một luồng trong ngữ cảnh cho một số yêu cầu? Nếu không, tôi nghĩ nên có.
Svick

8
Nó không được ghi nhận ở bất cứ đâu AFAIK.
Stephen Cleary

10
Cảm ơn - phản ứng tuyệt vời . Sự khác biệt trong hành vi giữa (rõ ràng) mã giống hệt về chức năng là bực bội nhưng có ý nghĩa với lời giải thích của bạn. Sẽ rất hữu ích nếu khung có thể phát hiện những bế tắc như vậy và đưa ra một ngoại lệ ở đâu đó.
Benjamin Fox

3
Có những tình huống sử dụng .ConfigureAwait (false) trong ngữ cảnh asp.net KHÔNG được khuyến nghị? Dường như với tôi rằng nó phải luôn được sử dụng và chỉ trong bối cảnh UI mà nó không nên được sử dụng vì bạn cần đồng bộ hóa với UI. Hay tôi đang thiếu điểm?
AlexGad

3
ASP.NET SynchronizationContextcung cấp một số chức năng quan trọng: nó chảy bối cảnh yêu cầu. Điều này bao gồm tất cả các loại công cụ từ xác thực đến cookie cho đến văn hóa. Vì vậy, trong ASP.NET, thay vì đồng bộ hóa trở lại UI, bạn đồng bộ hóa trở lại bối cảnh yêu cầu. Điều này có thể thay đổi trong thời gian ngắn: cái mới ApiControllerHttpRequestMessagebối cảnh như một tài sản - vì vậy nó có thể không bắt buộc phải chuyển ngữ cảnh qua SynchronizationContext- nhưng tôi chưa biết.
Stephen Cleary

62

Chỉnh sửa: Nói chung hãy cố gắng tránh làm những điều dưới đây trừ một nỗ lực cuối cùng để tránh bế tắc. Đọc bình luận đầu tiên từ Stephen Cleary.

Sửa nhanh từ đây . Thay vì viết:

Task tsk = AsyncOperation();
tsk.Wait();

Thử:

Task.Run(() => AsyncOperation()).Wait();

Hoặc nếu bạn cần một kết quả:

var result = Task.Run(() => AsyncOperation()).Result;

Từ nguồn (được chỉnh sửa để phù hợp với ví dụ trên):

AsyncOperation bây giờ sẽ được gọi trên ThreadPool, nơi sẽ không có SyncizationContext và các phần tiếp theo được sử dụng bên trong AsyncOperation sẽ không bị buộc quay lại chuỗi gọi.

Đối với tôi, nó trông giống như một tùy chọn có thể sử dụng được vì tôi không có tùy chọn làm cho nó không đồng bộ theo mọi cách (mà tôi thích).

Từ nguồn:

Đảm bảo rằng sự chờ đợi trong phương thức FooAsync không tìm thấy bối cảnh để trở thành nguyên soái. Cách đơn giản nhất để làm điều đó là gọi công việc không đồng bộ từ ThreadPool, chẳng hạn như bằng cách gói lời gọi trong Task.Run, ví dụ:

int Sync () {return Task.Run (() => Library.FooAsync ()). Kết quả; }

FooAsync bây giờ sẽ được gọi trên ThreadPool, nơi sẽ không có SyncizationContext và các phần tiếp theo được sử dụng bên trong FooAsync sẽ không bị buộc quay lại chủ đề gọi Sync ().


7
Có thể muốn đọc lại liên kết nguồn của bạn; tác giả đề nghị không nên làm điều này. Nó có hoạt động không? Có, nhưng chỉ theo nghĩa là bạn tránh bế tắc. Giải pháp này phủ nhận tất cả các lợi ích của asyncmã trên ASP.NET và trên thực tế có thể gây ra sự cố ở quy mô. BTW, ConfigureAwaitkhông "phá vỡ hành vi không đồng bộ phù hợp" trong bất kỳ kịch bản nào; đó chính xác là những gì bạn nên sử dụng trong mã thư viện.
Stephen Cleary

2
Đó là toàn bộ phần đầu tiên, có tiêu đề in đậm Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Toàn bộ phần còn lại của bài viết đang giải thích một vài cách khác nhau để làm điều đó nếu bạn thực sự cần .
Stephen Cleary

1
Đã thêm phần tôi tìm thấy trong nguồn - Tôi sẽ để nó cho độc giả tương lai quyết định. Lưu ý rằng bạn thường nên cố gắng tránh làm điều này và chỉ làm điều đó như là một phương án cuối cùng (ví dụ: khi sử dụng mã async, bạn không có quyền kiểm soát).
Ykok

3
Tôi thích tất cả các câu trả lời ở đây và như mọi khi .... tất cả chúng đều dựa trên ngữ cảnh (ý định chơi chữ lol). Tôi đang gói các cuộc gọi Async của httpClient bằng một phiên bản đồng bộ để tôi không thể thay đổi mã đó để thêm ConfigureAwait vào thư viện đó. Vì vậy, để ngăn chặn các bế tắc trong sản xuất, tôi đang gói các cuộc gọi Async trong Nhiệm vụ.Run. Vì vậy, theo tôi hiểu, điều này sẽ sử dụng thêm 1 luồng cho mỗi yêu cầu và tránh bế tắc. Tôi cho rằng để hoàn toàn tuân thủ, tôi cần sử dụng các phương thức đồng bộ hóa của WebClient. Đó là rất nhiều công việc để biện minh vì vậy tôi sẽ cần một lý do thuyết phục không phù hợp với phương pháp hiện tại của tôi.
samneric

1
Tôi đã kết thúc việc tạo Phương thức tiện ích mở rộng để chuyển đổi Async thành Sync. Tôi đã đọc ở đây một nơi nào đó giống như cách .Net framework thực hiện: công khai tĩnh TResult RunSync <TResult> (Func <Nhiệm vụ <TResult >> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter (). .GetResult (); }
samneric

10

Vì bạn đang sử dụng .Resulthay .Waithay awaitnày sẽ kết thúc gây ra bế tắc trong mã của bạn.

bạn có thể sử dụng ConfigureAwait(false)trong asynccác phương pháp để ngăn chặn bế tắc

như thế này:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

bạn có thể sử dụng ConfigureAwait(false)bất cứ nơi nào có thể cho Đừng chặn mã Async.


2

Hai trường này không thực sự loại trừ.

Đây là kịch bản mà bạn chỉ cần sử dụng

   Task.Run(() => AsyncOperation()).Wait(); 

hoặc một cái gì đó như

   AsyncContext.Run(AsyncOperation);

Tôi có một hành động MVC thuộc thuộc tính giao dịch cơ sở dữ liệu. Ý tưởng là (có thể) để khôi phục lại mọi thứ được thực hiện trong hành động nếu có sự cố xảy ra. Điều này không cho phép chuyển đổi ngữ cảnh, nếu không, giao dịch hoặc cam kết sẽ tự thất bại.

Thư viện tôi cần là async vì dự kiến ​​sẽ chạy async.

Lựa chọn duy nhất. Chạy nó như một cuộc gọi đồng bộ bình thường.

Tôi chỉ nói với mỗi của riêng mình.


vì vậy bạn đang đề xuất lựa chọn đầu tiên trong câu trả lời của bạn?
Don Cheadle

1

Tôi sẽ đặt nó ở đây nhiều hơn cho đầy đủ hơn là liên quan trực tiếp đến OP. Tôi đã dành gần một ngày để gỡ lỗi một HttpClientyêu cầu, tự hỏi tại sao tôi không bao giờ nhận được phản hồi.

Cuối cùng thấy rằng tôi đã quên awaitnhững asynccuộc gọi tiếp tục xuống các cuộc gọi stack.

Cảm thấy tốt như thiếu một dấu chấm phẩy.


-1

Tôi đang tìm ở đây:

http://msdn.microsoft.com/en-us/l Library / system.r nb.compilerservice.taskawaiter (v = vs.110) .aspx

Và đây:

http://msdn.microsoft.com/en-us/l Library / system.r nb.compilerservice.taskawaiter.getresult (v = vs.110) .aspx

Và nhìn thấy:

Loại này và các thành viên của nó được dự định sử dụng bởi trình biên dịch.

Xem xét awaitphiên bản hoạt động, và là cách làm 'đúng', bạn có thực sự cần một câu trả lời cho câu hỏi này không?

Phiếu bầu của tôi là: Lạm dụng API .


Tôi đã không nhận thấy rằng, mặc dù tôi đã thấy các ngôn ngữ khác xung quanh cho biết rằng sử dụng API GetResult () là trường hợp sử dụng được hỗ trợ (và dự kiến).
Benjamin Fox

1
Hơn nữa, nếu bạn tái cấu trúc Test5Controller.Get()để loại bỏ người phục vụ bằng cách sau: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Hành vi tương tự có thể được quan sát.
Benjamin Fox
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.