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请求上下文中)。 HttpClient.GetAsync
返回一个未完成的Task
。 AsyncAwait_GetSomeDataAsync
等待Task
; 由于它没有完成, AsyncAwait_GetSomeDataAsync
返回一个未完成的Task
。 Test5Controller.Get
阻止当前线程,直到该Task
完成。 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
的简要说明。 SynchronizationContext
只限制一个线程的请求上下文。 2012-07-13更新:将此答案合并到博客文章中。
编辑:通常尽量避免做下面的事情,除非作为最后的努力避免死锁。 阅读Stephen Cleary的第一条评论。
从这里快速修复。 而不是写作:
Task tsk = AsyncOperation();
tsk.Wait();
尝试:
Task.Run(() => AsyncOperation()).Wait();
或者如果您需要结果:
var result = Task.Run(() => AsyncOperation()).Result;
来源(编辑以匹配上面的例子):
现在将在ThreadPool上调用AsyncOperation,其中不会有SynchronizationContext,并且在AsyncOperation内部使用的延续不会被强制回到调用线程。
对我来说,这看起来像一个可用的选项,因为我没有选择使其异步(我更喜欢)。
来源:
确保FooAsync方法中的await没有找到上送回的上下文。 最简单的方法是从ThreadPool调用异步工作,比如通过将调用包装在Task.Run中,例如
int Sync(){return Task.Run(()=> Library.FooAsync())。 }
现在将在ThreadPool上调用FooAsync,其中不会有SynchronizationContext,并且在FooAsync中使用的延续不会被强制回到调用Sync()的线程。
由于您正在使用.Result
或.Wait
或await
这将最终导致您的代码中的死锁 。
您可以在async
方法中使用ConfigureAwait(false)
来防止死锁
喜欢这个:
var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
您可以尽可能使用ConfigureAwait(false)
来阻止异步代码。
上一篇: HttpClient.GetAsync(...) never returns when using await/async