HttpClient.GetAsync(…) never returns when using await/async
编辑:这个问题看起来可能是同一个问题,但没有回答…
编辑:在测试用例5中,任务似乎停留在
我在.NET 4.5中使用system.net.http.httpclient时遇到了一些奇怪的行为,在这种情况下,"等待"对(例如)
只有在使用新的异步/等待语言功能和任务API时,才会出现这种情况——当只使用延续时,代码似乎总是可以工作的。
下面是一些重现问题的代码-将其放到Visual Studio 11中的新"MVC 4 WebAPI项目"中,以公开以下get端点:
1 2 3 4 5 6 | /api/test1 /api/test2 /api/test3 /api/test4 /api/test5 <--- never completes /api/test6 |
这里的每个端点都返回相同的数据(stackoverflow.com的响应头),但
要复制的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | 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(); } } |
您滥用了API。
情况如下:在ASP.NET中,一次只能有一个线程处理请求。如果需要,您可以执行一些并行处理(从线程池借用其他线程),但只有一个线程具有请求上下文(其他线程没有请求上下文)。
这是由ASP.NET
默认情况下,当您使用
所以,这就是
Test5Controller.Get 执行AsyncAwait_GetSomeDataAsync (在ASP.NET请求上下文中)。AsyncAwait_GetSomeDataAsync 执行HttpClient.GetAsync (在ASP.NET请求上下文中)。- 发送HTTP请求,
HttpClient.GetAsync 返回未完成的Task 。 AsyncAwait_GetSomeDataAsync 等待Task ;由于它不完整,AsyncAwait_GetSomeDataAsync 返回未完成的Task 。Test5Controller.Get 阻塞当前线程,直到该Task 完成。- HTTP响应进入,
HttpClient.GetAsync 返回的Task 完成。 AsyncAwait_GetSomeDataAsync 尝试在ASP.NET请求上下文中恢复。但是,该上下文中已经有一个线程:该线程在Test5Controller.Get 中被阻塞。- 死锁。
这就是为什么其他方法有效:
- (
test1 、test2 和test3 :Continuations_GetSomeDataAsync 在ASP.NET请求上下文之外,将继续调度到线程池。这使得由Continuations_GetSomeDataAsync 返回的Task 可以在不必重新进入请求上下文的情况下完成。 - (
test4 和test6 :由于Task 等待,ASP.NET请求线程没有被阻塞。这允许AsyncAwait_GetSomeDataAsync 在准备好继续时使用ASP.NET请求上下文。
以下是最佳实践:
这样,您就得到了两个好处:继续(
更多信息:
- 我的
async /await 介绍帖子,其中简要介绍了Task 等待者如何使用SynchronizationContext 。 - Async/Await常见问题解答,将详细介绍上下文。还可以看到等待、用户界面和死锁!哦,我的天哪!尽管您是在ASP.NET中而不是在UI中,但这里确实适用,因为ASP.NET
SynchronizationContext 一次只将请求上下文限制为一个线程。 - 此msdn论坛帖子。
- StephenToub演示了这个死锁(使用UI),LucianWischik也演示了这个死锁。
更新2012-07-13:将此答案并入博客帖子。
编辑:通常尽量避免做下面的工作,除了最后一点努力避免死锁。阅读斯蒂芬·克利里的第一条评论。
从这里快速修复。不是写:
1 2 | Task tsk = AsyncOperation(); tsk.Wait(); |
尝试:
1 | Task.Run(() => AsyncOperation()).Wait(); |
或者如果你需要一个结果:
1 | var result = Task.Run(() => AsyncOperation()).Result; |
从源(编辑以匹配上述示例):
AsyncOperation will now be invoked on the ThreadPool, where there
won’t be a SynchronizationContext, and the continuations used inside
of AsyncOperation won’t be forced back to the invoking thread.
对于我来说,这看起来是一个可用的选项,因为我没有让它一直异步的选项(我更喜欢)。
来源:
Ensure that the await in the FooAsync method doesn’t find a context to
marshal back to. The simplest way to do that is to invoke the
asynchronous work from the ThreadPool, such as by wrapping the
invocation in a Task.Run, e.g.int Sync() {
return Task.Run(() => Library.FooAsync()).Result; }FooAsync will now be invoked on the ThreadPool, where there won’t be a
SynchronizationContext, and the continuations used inside of FooAsync
won’t be forced back to the thread that’s invoking Sync().
由于您使用的是
在
这样地:
1 2 | var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); |
you can use
ConfigureAwait(false) wherever possible for Don't Block Async Code .
这两所学校并不排除在外。
以下是您只需使用的场景
1 | Task.Run(() => AsyncOperation()).Wait(); |
或者类似的
1 | AsyncContext.Run(AsyncOperation); |
我有一个MVC操作,它在数据库事务属性下。这个想法是(很可能)如果出了什么问题,就把行动中所做的一切都回滚。这不允许上下文切换,否则事务回滚或提交本身将失败。
我需要的库是异步的,因为它应该运行异步的。
唯一的选择。将其作为普通同步调用运行。
我只是对每个人说自己的话。
我将把它放在这里更多的是为了完整性而不是与操作直接相关。我花了将近一天的时间调试了一个
最后发现我忘了把
感觉就像丢了一个分号一样好。
我看这里:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskwaiter(v=vs.110).aspx
这里:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskwaiter.getresult(v=vs.110).aspx
看到:
This type and its members are intended for use by the compiler.
考虑到
我的投票是:滥用API。