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被阻塞的线程。
  • 僵局。

这是为什么其他的工作:

  • test1test2test3 ): Continuations_GetSomeDataAsync在ASP.NET请求上下文之外调度线程池的延续。 这允许Continuations_GetSomeDataAsync返回的Task完成,而不必重新输入请求上下文。
  • test4test6 ):由于Task正在等待 ,所以ASP.NET请求线程不会被阻塞。 这允许AsyncAwait_GetSomeDataAsync在准备好继续时使用ASP.NET请求上下文。

以下是最佳做法:

  1. 在你的“库” async方法中,尽可能使用ConfigureAwait(false) 。 在你的情况下,这将改变AsyncAwait_GetSomeDataAsync var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要阻止Task ; 它一直是async的。 换句话说,使用await而不是GetResultTask.ResultTask.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

和这里:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

看到:

这个类型及其成员是为编译器使用的。

考虑到await版本的作品,而且是“正确的”做事方式,你真的需要对这个问题的答案吗?

我的投票是: 滥用API

这两所学校并不是真的排除在外。

这里是你只需要使用的场景

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

或类似的东西

  AsyncContext.Run(AsyncOperation); 

我有一个在数据库事务属性下的MVC操作。 这个想法是(可能)回滚在动作中做的所有事情。 这不允许上下文切换,否则事务回滚或提交将自行失败。

我需要的库是异步的,因为它可以运行异步。

唯一的选择。 作为一个正常的同步呼叫运行。

我只是对每一个人说。