问题描述
编辑: This问题看起来可能是同样的问题,但一直没有回应......
编辑:在测试用例5任务似乎停留在 WaitingForActivation
状态
我遇到了在.NET 4.5使用System.Net.Http.HttpClient一些奇怪的行为 - 在等待呼叫的结果(例如) httpClient.GetAsync(... )
将永远不会返回。
这只能使用新异步发生时,在某些情况下/等待语言功能和任务的API - 在code似乎总是只用延续的时候工作到
。下面是一些code抄录问题 - 这拖放到一个新的MVC 4的WebAPI项目在Visual Studio 11,露出下面的GET端点:
/ API / TEST1
/ API / TEST2
/ API / TEST3
/ API / TEST4
/ API / TEST5< ---永远不会完成
/ API / TEST6
除了 / API / TEST5
这里的每个端点返回相同的数据(从stackoverflow.com响应头),它永远不会完成。
让我在HttpClient的类中遇到的错误,还是我滥用API以某种方式?
code复制:
公共类BaseApiController:ApiController
{
///<总结>
///获取使用数据的延续
///< /总结>
保护任务<字符串> Continuations_GetSomeDataAsync()
{
VAR的HttpClient =新的HttpClient();
VAR T = httpClient.GetAsync(http://stackoverflow.com,HttpCompletionOption.ResponseHeadersRead);
返回t.ContinueWith(T1 => t1.Result.Content.Headers.ToString());
}
///<总结>
///获取使用异步/计谋数据
///< /总结>
保护异步任务<字符串> AsyncAwait_GetSomeDataAsync()
{
VAR的HttpClient =新的HttpClient();
VAR的结果=等待httpClient.GetAsync(http://stackoverflow.com,HttpCompletionOption.ResponseHeadersRead);
返回result.Content.Headers.ToString();
}
}
公共类Test1Controller:BaseApiController
{
///<总结>
///手柄使用异步/等待任务
///< /总结>
公共异步任务<字符串>得到()
{
VAR数据=等待Continuations_GetSomeDataAsync();
返回的数据;
}
}
公共类Test2Controller:BaseApiController
{
///<总结>
///通过阻止线程,直到任务完成手柄任务
///< /总结>
公共字符串获得()
{
变种任务= Continuations_GetSomeDataAsync();
VAR数据= task.GetAwaiter()调用getResult()。
返回的数据;
}
}
公共类Test3Controller:BaseApiController
{
///<总结>
///传递任务回到控制器主机
///< /总结>
公共任务<字符串>得到()
{
返回Continuations_GetSomeDataAsync();
}
}
公共类Test4Controller:BaseApiController
{
///<总结>
///手柄使用异步/等待任务
///< /总结>
公共异步任务<字符串>得到()
{
VAR数据=等待AsyncAwait_GetSomeDataAsync();
返回的数据;
}
}
公共类Test5Controller:BaseApiController
{
///<总结>
///通过阻止线程,直到任务完成手柄任务
///< /总结>
公共字符串获得()
{
变种任务= AsyncAwait_GetSomeDataAsync();
VAR数据= task.GetAwaiter()调用getResult()。
返回的数据;
}
}
公共类Test6Controller:BaseApiController
{
///<总结>
///传递任务回到控制器主机
///< /总结>
公共任务<字符串>得到()
{
返回AsyncAwait_GetSomeDataAsync();
}
}
正在滥用API。
下面的情况:在ASP.NET中,只有一个线程可以同时处理的请求。你可以做一些并行处理(如有必要,从线程池借用其他线程),但只有一个线程将不得不请求上下文(额外的线程不必请求上下文)。
这是由ASP.NET管理 的SynchronizationContext
。
在默认情况下,当你计谋
A 工作
,该方法继续在捕获 SynchronizationContext的
(或捕获的TaskScheduler
,如果没有的SynchronizationContext
)。通常情况下,这只是你想要什么:一个异步控制器动作将等待
的东西,它恢复时,它与请求上下文恢复
所以,这里的原因 TEST5
失败:
-
Test5Controller.Get
执行AsyncAwait_GetSomeDataAsync
(在ASP.NET请求上下文)。 -
AsyncAwait_GetSomeDataAsync
执行HttpClient.GetAsync
(在ASP.NET请求上下文)。 - 在HTTP请求发送出去,而
HttpClient.GetAsync
返回一个未完成的工作
。 -
AsyncAwait_GetSomeDataAsync
等待工作
;因为它是不完整的,AsyncAwait_GetSomeDataAsync
返回一个未完成的工作
。 -
Test5Controller.Get
块当前线程,直到工作
完成。 - 在HTTP响应进来,而
工作
按HttpClient.GetAsync
返回完成。 -
AsyncAwait_GetSomeDataAsync
试图ASP.NET请求范围内恢复。不过,已经有一个线程在这种情况下:受阻于Test5Controller.Get
线程 - 僵局。
这也是为什么在其他的工作:
- (
测试1
,测试2
和TEST3
) :Continuations_GetSomeDataAsync
调度延续到线程池,之外的ASP.NET请求范围内。这使得工作
,而无需重新输入请求上下文由Continuations_GetSomeDataAsync
完成返回。 - (
TEST4
和TEST6
):由于工作
是期待已久的的,ASP.NET请求线程没有被阻塞。这使得AsyncAwait_GetSomeDataAsync
使用ASP.NET请求上下文时,它已准备好继续。
而这里的最佳实践:
- 在库
异步
方法,使用ConfigureAwait(假)
只要有可能。在你的情况,这将改变AsyncAwait_GetSomeDataAsync
是VAR的结果=等待httpClient.GetAsync(http://stackoverflow.com,HttpCompletionOption.ResponseHeadersRead ).ConfigureAwait(假);
- 请不要在
工作
S挡;这是异步
一路下跌。换句话说,使用等待
而不是调用getResult
(<$ C C $> Task.Result 和Task.Wait
也应更换与等待
)。
这样的话,你会得到两个好处:延续(在该 AsyncAwait_GetSomeDataAsync
法的其余部分)是运行在一个没有进入一个基本的线程池线程ASP.NET请求上下文;而控制器本身是异步
(这不会阻止请求的线程)。
详细信息:
- 我的<$c$c>async$c$c>/<$c$c>await$c$c>开场后,其中包括
工作
awaiters如何使用的SynchronizationContext
的简要说明。 - 的异步/等待FAQ ,其中将详细在上下文。另请参见等待,和用户界面,和死锁!噢,我的!其中的确实的适用于此,即使你在ASP.NET而不是用户界面,因为ASP.NET
的SynchronizationContext
限制了请求上下文只是一个线程在同一时间。 - 这MSDN论坛帖子。
- 斯蒂芬Toub 演示这一僵局(使用UI)和so确实卢西恩Wischik 。
更新2012-07-13:将这一答案的。
Edit: This question looks like it might be the same problem, but has no responses...
Edit: In test case 5 the task appears to be stuck in WaitingForActivation
state.
I've encountered some odd behaviour using the System.Net.Http.HttpClient in .NET 4.5 - where "awaiting" the result of a call to (e.g.) httpClient.GetAsync(...)
will never return.
This only occurs in certain circumstances when using the new async/await language functionality and Tasks API - the code always seems to work when using only continuations.
Here's some code which reproduces the problem - drop this into a new "MVC 4 WebApi project" in Visual Studio 11 to expose the following GET endpoints:
/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6
Each of the endpoints here return the same data (the response headers from stackoverflow.com) except for /api/test5
which never completes.
Have I encountered a bug in the HttpClient class, or am I misusing the API in some way?
Code to reproduce:
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();
}
}
You are misusing the API.
Here's the situation: in ASP.NET, only one thread can handle a request at a time. You can do some parallel processing if necessary (borrowing additional threads from the thread pool), but only one thread would have the request context (the additional threads do not have the request context).
This is managed by the ASP.NET SynchronizationContext
.
By default, when you await
a Task
, the method resumes on a captured SynchronizationContext
(or a captured TaskScheduler
, if there is no SynchronizationContext
). Normally, this is just what you want: an asynchronous controller action will await
something, and when it resumes, it resumes with the request context.
So, here's why test5
fails:
Test5Controller.Get
executesAsyncAwait_GetSomeDataAsync
(within the ASP.NET request context).AsyncAwait_GetSomeDataAsync
executesHttpClient.GetAsync
(within the ASP.NET request context).- The HTTP request is sent out, and
HttpClient.GetAsync
returns an uncompletedTask
. AsyncAwait_GetSomeDataAsync
awaits theTask
; since it is not complete,AsyncAwait_GetSomeDataAsync
returns an uncompletedTask
.Test5Controller.Get
blocks the current thread until thatTask
completes.- The HTTP response comes in, and the
Task
returned byHttpClient.GetAsync
is completed. AsyncAwait_GetSomeDataAsync
attempts to resume within the ASP.NET request context. However, there is already a thread in that context: the thread blocked inTest5Controller.Get
.- Deadlock.
Here's why the other ones work:
- (
test1
,test2
, andtest3
):Continuations_GetSomeDataAsync
schedules the continuation to the thread pool, outside the ASP.NET request context. This allows theTask
returned byContinuations_GetSomeDataAsync
to complete without having to re-enter the request context. - (
test4
andtest6
): Since theTask
is awaited, the ASP.NET request thread is not blocked. This allowsAsyncAwait_GetSomeDataAsync
to use the ASP.NET request context when it is ready to continue.
And here's the best practices:
- In your "library"
async
methods, useConfigureAwait(false)
whenever possible. In your case, this would changeAsyncAwait_GetSomeDataAsync
to bevar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- Don't block on
Task
s; it'sasync
all the way down. In other words, useawait
instead ofGetResult
(Task.Result
andTask.Wait
should also be replaced withawait
).
That way, you get both benefits: the continuation (the remainder of the AsyncAwait_GetSomeDataAsync
method) is run on a basic thread pool thread that doesn't have to enter the ASP.NET request context; and the controller itself is async
(which doesn't block a request thread).
More information:
- My
async
/await
intro post, which includes a brief description of howTask
awaiters useSynchronizationContext
. - The Async/Await FAQ, which goes into more detail on the contexts. Also see Await, and UI, and deadlocks! Oh, my! which does apply here even though you're in ASP.NET rather than a UI, because the ASP.NET
SynchronizationContext
restricts the request context to just one thread at a time. - This MSDN forum post.
- Stephen Toub demos this deadlock (using a UI), and so does Lucian Wischik.
Update 2012-07-13: Incorporated this answer into a blog post.
这篇关于HttpClient.GetAsync(...)永远不会在使用计谋/异步返回的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!