当使用await/async时,HttpClient.GetAsync(...)永远不会返回。
当使用await/async时,HttpClient.GetAsync(...)永远不会返回。
编辑:这个问题看起来可能是相同的问题,但没有回应...
编辑:在测试案例5中,任务似乎被卡在WaitingForActivation
状态。
我在使用.NET 4.5中的System.Net.Http.HttpClient时遇到了一些奇怪的行为 - 当"等待"调用(例如httpClient.GetAsync(...)
)的结果永远不会返回。
这只在使用新的async/await语言功能和Tasks API时发生在某些情况下 - 当只使用连续时,代码似乎总是工作的。
以下是重现问题的代码 - 将其放入Visual Studio 11中的新的"MVC 4 WebApi项目"中以公开以下GET端点:
/api/test1 /api/test2 /api/test3 /api/test4 /api/test5 <--- 永远不会完成 /api/test6
这里的每个端点都返回相同的数据(来自stackoverflow.com的响应标头),除了/api/test5
永远不会完成。
我是否遇到了HttpClient类的错误,还是以某种方式误用了API?
重现代码:
public class BaseApiController : ApiController { ////// 使用连续获取数据 /// protected TaskContinuations_GetSomeDataAsync() { var httpClient = new HttpClient(); var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString()); } /// /// 使用async/await获取数据 /// protected async TaskAsyncAwait_GetSomeDataAsync() { var httpClient = new HttpClient(); var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return result.Content.Headers.ToString(); } } public class Test1Controller : BaseApiController { /// /// 使用Async/Await处理任务 /// public async TaskGet() { var data = await Continuations_GetSomeDataAsync(); return data; } } public class Test2Controller : BaseApiController { /// /// 通过阻塞线程直到任务完成处理任务 /// public string Get() { var task = Continuations_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test3Controller : BaseApiController { ////// 将任务返回给控制器主机 /// public TaskGet() { return Continuations_GetSomeDataAsync(); } } public class Test4Controller : BaseApiController { /// /// 使用Async/Await处理任务 /// public async TaskGet() { var data = await AsyncAwait_GetSomeDataAsync(); return data; } } public class Test5Controller : BaseApiController { /// /// 通过阻塞线程直到任务完成处理任务 /// public string Get() { var task = AsyncAwait_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test6Controller : BaseApiController { ////// 将任务返回给控制器主机 /// public TaskGet() { return AsyncAwait_GetSomeDataAsync(); } }
问题:使用await/async时,调用HttpClient.GetAsync(...)时永远不会返回。
原因:在某些情况下,使用await/async时,调用HttpClient.GetAsync(...)可能会出现死锁问题。这是因为在调用异步方法时,可能会发生线程上下文切换,导致等待的异步操作无法继续执行。
解决方法:有几种解决方法可以避免这个问题:
1. 尝试使用Task.Run()将异步操作包装在一个新的线程中,然后使用Wait()方法等待操作完成。示例代码如下:
Task.Run(() => AsyncOperation()).Wait();
2. 如果需要获取异步操作的结果,可以使用Task.Run()包装异步操作,并使用Result属性获取结果。示例代码如下:
var result = Task.Run(() => AsyncOperation()).Result;
通过在线程池中调用异步操作,可以避免SynchronizationContext的存在,从而避免了死锁问题的发生。
需要注意的是,上述解决方法只是一种不得已的解决方案,不建议频繁使用。在ASP.NET中使用async/await能够带来更多的好处,而这种解决方案则会抵消这些好处。
同时,应该避免在异步方法中使用同步包装器,以免暴露出同样的死锁问题。在库代码中,可以使用ConfigureAwait(false)来确保不会发生线程上下文切换,从而避免死锁问题的发生。
使用await/async时,调用HttpClient.GetAsync(...)可能会导致死锁问题。为了解决这个问题,可以使用Task.Run()将异步操作包装在新线程中,或者使用ConfigureAwait(false)来避免线程上下文切换。然而,这些解决方法应该谨慎使用,避免频繁使用。最好的方式是在设计代码时避免出现死锁问题。
使用await/async时,调用HttpClient.GetAsync(...)方法时,可能会出现永远不返回的情况。这个问题的原因是API的误用。在ASP.NET中,每次只能有一个线程处理一个请求。如果需要进行一些并行处理,可以从线程池中借用额外的线程,但只有一个线程会有请求上下文。这是由ASP.NET的SynchronizationContext进行管理的。当使用await关键字等待一个Task时,默认情况下,方法会在一个捕获的SynchronizationContext(或者如果没有SynchronizationContext,则是一个捕获的TaskScheduler)上继续执行。通常情况下,这是我们想要的:一个异步控制器操作会等待某些操作完成,然后在请求上下文中继续执行。
下面是出现问题的原因:
1. Test5Controller.Get方法执行AsyncAwait_GetSomeDataAsync方法(在ASP.NET请求上下文中)。
2. AsyncAwait_GetSomeDataAsync方法执行HttpClient.GetAsync方法(在ASP.NET请求上下文中)。
3. 发送HTTP请求,HttpClient.GetAsync方法返回一个未完成的Task。
4. AsyncAwait_GetSomeDataAsync方法等待这个Task,由于它还没有完成,AsyncAwait_GetSomeDataAsync方法返回一个未完成的Task。
5. Test5Controller.Get方法阻塞当前线程,直到这个Task完成。
6. 收到HTTP响应,HttpClient.GetAsync方法返回的Task完成。
7. AsyncAwait_GetSomeDataAsync方法试图在ASP.NET请求上下文中恢复。然而,这个上下文中已经有一个线程:被Test5Controller.Get方法阻塞的线程。
8. 死锁。
下面是解决方法:
1. 在“库”中的异步方法中,尽可能使用ConfigureAwait(false)。在这种情况下,AsyncAwait_GetSomeDataAsync方法应该改为var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)。
2. 不要在Task上进行阻塞操作,应该全程使用async/await。换句话说,应该使用await而不是GetResult(Task.Result)和Wait(Task.Wait)。
最佳实践:
1. 使用ConfigureAwait(false)来避免死锁问题。
2. 使用async/await来避免在Task上进行阻塞操作,这样可以保持请求线程的响应性。
更多信息:
- 我的async/await介绍文章,其中包含了Task awaiter如何使用SynchronizationContext的简要描述。
- Async/Await FAQ,对上下文进行了更详细的解释。
- MSDN论坛帖子。
- Stephen Toub在演示中展示了这种死锁(使用UI),Lucian Wischik也做过类似的演示。
- 更新日期:2012-07-13,将这个答案整理到了博客文章中。
ASP.NET的SynchronizationContext文档中是否有说明一个请求上下文中只能有一个线程?我认为应该有这样的文档。
目前我没有找到这方面的文档。
非常感谢,您的回答非常棒。同样的代码在不同情况下产生不同的行为,这让人很沮丧,但是通过您的解释,一切都变得合理了。如果框架能够检测到这种死锁并在某个地方引发异常,那将非常有用。
在ASP.NET上下文中,是否有不建议使用ConfigureAwait(false)的情况?在我的理解中,无论什么情况下都应该使用它,只有在UI上下文中才不应该使用,因为需要与UI同步。或者我是否理解错了?
ASP.NET的SynchronizationContext提供了一些重要的功能:它传递了请求上下文,包括身份验证、Cookie和区域设置等各种信息。因此,在ASP.NET中,我们不是与UI同步,而是与请求上下文同步。这可能会很快发生改变:新的ApiController有一个HttpRequestMessage上下文作为属性,因此可能不需要通过SynchronizationContext来传递上下文了,但我还不确定。
谢谢!我之前错误地使用了Task.WaitAll,而不是await Task.WhenAll。您的回答让我找到了正确的方向。
在你的介绍中,你有一个没有async的public Task MyOldTaskParallelLibraryCode() {}(没有async的Task),在这里你返回了什么?因为编译器要求你返回一个Task。你如何创建一个空的Task?
旧式的TPL代码通常使用Task.Factory.StartNew或TaskCompletionSource。需要注意的是,对于大多数现代代码,不建议使用StartNew。
我对旧式(也对新式)不太熟悉。我使用的是新式的方式,我想要一个CPU密集型的任务以异步方式运行,但是其中没有任何await方法(更像是一个流的自定义解析器)。你建议我返回什么?
同步方法应该有同步的API。如果你想要异步地调用它(即保持UI的响应性),可以使用Task.Run来调用它。我有一系列关于Task.Run的礼仪博客文章,可以详细了解。
我差点陷入将同步方法封装为异步的陷阱。异步的方式不仅更容易理解何时以及如何使用async,而且使代码更简洁!如果可以,我会给你加100分的。
那么为什么在控制器的中间调用response.GetStringAsAsync().Result不会导致死锁呢?基本上,为什么在.NET Web应用程序中在.NET对象内部调用.Result时没有任何死锁问题?看起来应该是同样的问题-在一个UI上下文中访问它,在库中会发生某处的await。
死锁是由捕获的上下文(例如SynchronizationContext)的存在以及在该任务上阻塞上下文引起的。控制台应用程序没有SynchronizationContext。类似地,GetStringAsAsync()方法不会捕获上下文(在该平台上)。
是否有说明这些方法不会捕获/阻塞UI上下文的文档?某些方法会捕获上下文,某些方法不会捕获上下文,这看起来很奇怪(msdn.microsoft.com/en-us/library/... 是我想提到的实际方法)。
微软的API尽量不捕获上下文。有时会因为错误而捕获上下文(这是一个容易出错的Bug)。我不知道有没有相关的文档,您不应该依赖这一点,因为它是一种实现细节。
那么您建议从头到尾都使用await,是因为这样最安全,不会受到实现的更改等影响。如果我们确实需要在请求期间阻塞,应该使用Task.Run(() => ...)将其放入线程池。
是的,这是始终使用async的一个原因。如果确实需要阻塞,最好始终使用同步API,例如WebClient。它们是为阻塞而设计的。
谢谢,这让我疯狂。所有的调用都完美地工作,只有一个调用出现问题,但是代码和其他调用相同。原来是在调用链中有一个额外的步骤没有使用"configureawait(false)",导致了死锁。