HttpClient.GetAsync(…)在使用await / async时永远不会返回
编辑: 这个问题看起来可能是相同的问题,但没有回应…
编辑:在测试案例5中,任务似乎卡在WaitingForActivation
状态。
我遇到一些奇怪的行为在.NET 4.5中使用System.Net.Http.HttpClient – “等待”(例如) httpClient.GetAsync(...)
调用的结果将永远不会返回。
只有在使用新的异步/等待语言功能和任务API时,才会出现这种情况 – 只使用延续时,代码似乎总能工作。
下面是一些重现问题的代码 – 在Visual Studio 11中将其放入一个新的“MVC 4 WebApi项目”中,以显示以下GET端点:
/api/test1 /api/test2 /api/test3 /api/test4 /api/test5 <--- never completes /api/test6
这里的每个端点都返回与从未完成的/api/test5
相同的数据(来自stackoverflow.com的响应头)。
我在HttpClient类中遇到了一个错误,或者我以某种方式滥用API?
代码重现:
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 SynchronizationContext
管理 。
默认情况下,当您await
Task
,该方法将在捕获的SynchronizationContext
(或捕获的TaskScheduler
,如果没有SynchronizationContext
)上恢复。 通常情况下,这正是您想要的:异步控制器操作将await
某些内容,并在恢复时,继续执行请求上下文。
所以,这就是test5
失败的原因:
-
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
方法中,尽可能使用ConfigureAwait(false)
。 在你的情况下,这将改变AsyncAwait_GetSomeDataAsync
var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- 不要阻止
Task
; 它一直是async
的。 换句话说,使用await
而不是GetResult
(Task.Result
和Task.Wait
也应该替换为await
)。
这样,您将获得以下两个好处:继续( AsyncAwait_GetSomeDataAsync
方法的其余部分)在基本线程池线程上运行,不必输入ASP.NET请求上下文; 而控制器本身是async
(不会阻塞请求线程)。
更多信息:
- 我的
async
/await
介绍帖子 ,其中包括Task
等待者如何使用SynchronizationContext
的简要说明。 - Async / Await常见问题解答 ,在上下文中有更详细的介绍。 另请参阅等待和UI,以及死锁! 天啊! 即使你在ASP.NET中而不是在UI中,它也适用于这里,因为ASP.NET
SynchronizationContext
只限制一个线程的请求上下文。 - 此MSDN论坛帖子 。
- Stephen Toub 演示了这个僵局(使用UI) , Lucian Wischik也是如此 。
更新2012-07-13:将这个答案编入博客文章 。
从这里快速修复。 而不是写作:
Task tsk = AsyncOperation(); tsk.Wait();
尝试:
Task.Run(() => AsyncOperation()).Wait();
或者如果你需要一个结果:
var result = Task.Run(() => AsyncOperation()).Result;
来源(编辑以匹配上面的例子):
现在将在ThreadPool上调用AsyncOperation,其中不会有SynchronizationContext,并且在AsyncOperation内部使用的延续不会被强制回到调用线程。
对我来说,这看起来像一个很好的方法,因为我不需要在整个地方使用ConfigureAwait
(就我所知,也可以在GUI中打破正确的异步行为),也不必一直使它成为异步在我的应用程序(目前不是一个选项)。
来源:
确保FooAsync方法中的await没有找到要回传的上下文。 最简单的方法是从ThreadPool调用异步工作,比如通过将调用包装在Task.Run中,例如
int Sync(){return Task.Run(()=> Library.FooAsync())。 }
现在将在ThreadPool上调用FooAsync,在那里不会有SynchronizationContext,并且在FooAsync中使用的延续不会被强制回到调用Sync()的线程。
我在找这里:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
和这里:
看到:
这个类型及其成员是为编译器使用的。
考虑到await
版本的作品,而且是“正确的”做事方式,你真的需要对这个问题的答案吗?
我的投票是: 滥用API 。
这两所学校并不是真的排除在外。
这里是你只需要使用的场景
Task.Run(() => AsyncOperation()).Wait();
或类似的东西
AsyncContext.Run(AsyncOperation);
我有一个在数据库事务属性下的MVC操作。 这个想法是(可能)回滚在动作中做的所有事情。 这不允许上下文切换,否则事务回滚或提交将自行失败。
我需要的库是异步的,因为它可以运行异步。
唯一的选择。 作为一个正常的同步呼叫运行。
我只是对每一个人说。