编辑:这个问题看起来可能是同样的问题,但没有回答…
编辑:在测试用例5中,任务似乎停留在WaitingForActivation状态。
我在。net 4.5中使用System.Net.Http.HttpClient时遇到过一些奇怪的行为——“等待”调用(例如httpClient.GetAsync(…))的结果将永远不会返回。
这只发生在使用新的async/await语言功能和任务API时的特定情况下-当只使用延续时,代码似乎总是工作。
下面是一些重现这个问题的代码——将其放入Visual Studio 11中的一个新的“MVC 4 WebApi项目”中,以公开以下GET端点:
/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6
这里的每个端点返回相同的数据(来自stackoverflow.com的响应头),除了/api/test5,它永远不会完成。
我是否在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管理的。净SynchronizationContext。
默认情况下,当您等待一个任务时,该方法将在一个捕获的SynchronizationContext(或者一个捕获的TaskScheduler,如果没有SynchronizationContext)上恢复。通常,这正是您想要的:异步控制器操作将等待某些内容,当它恢复时,它将与请求上下文一起恢复。
下面是test5失败的原因:
Test5Controller.Get executes AsyncAwait_GetSomeDataAsync (within the ASP.NET request context).
AsyncAwait_GetSomeDataAsync executes HttpClient.GetAsync (within the ASP.NET request context).
The HTTP request is sent out, and HttpClient.GetAsync returns an uncompleted Task.
AsyncAwait_GetSomeDataAsync awaits the Task; since it is not complete, AsyncAwait_GetSomeDataAsync returns an uncompleted Task.
Test5Controller.Get blocks the current thread until that Task completes.
The HTTP response comes in, and the Task returned by HttpClient.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 in Test5Controller.Get.
Deadlock.
以下是其他方法有效的原因:
(test1, test2,和test3): Continuations_GetSomeDataAsync调度线程池的延续,在ASP。NET请求上下文。这允许由Continuations_GetSomeDataAsync返回的任务完成,而不必重新进入请求上下文。
(test4和test6):由于任务正在等待,ASP。NET请求线程没有阻塞。这允许AsyncAwait_GetSomeDataAsync使用ASP。当它准备继续时,NET请求上下文。
以下是最佳实践:
在你的“库”异步方法中,尽可能使用ConfigureAwait(false)。在你的情况下,这将改变AsyncAwait_GetSomeDataAsync为var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
不要阻塞任务;它完全是异步的。换句话说,使用await而不是GetResult (Task。结果和任务。Wait也应该替换为await)。
这样,你可以得到两个好处:延续(AsyncAwait_GetSomeDataAsync方法的剩余部分)在基本线程池线程上运行,不需要进入ASP。NET请求上下文;而且控制器本身是异步的(它不会阻塞请求线程)。
更多信息:
我的async/await介绍帖子,其中包括任务等待器如何使用SynchronizationContext的简要描述。
Async/Await常见问题解答,其中详细介绍了上下文。还可以查看Await、UI和死锁!哦,我的天!这在这里也适用,即使你在ASP。NET而不是UI,因为ASP。NET SynchronizationContext将请求上下文限制为一次只有一个线程。
这是MSDN论坛的帖子。
Stephen Toub演示了这种死锁(使用一个UI), Lucian Wischik也演示了。
更新2012-07-13:将这个答案整合到一篇博客文章中。
您在滥用API。
情况是这样的:在ASP。NET中,一次只能有一个线程处理一个请求。如果需要,您可以执行一些并行处理(从线程池中借用额外的线程),但是只有一个线程具有请求上下文(额外的线程没有请求上下文)。
这是由ASP管理的。净SynchronizationContext。
默认情况下,当您等待一个任务时,该方法将在一个捕获的SynchronizationContext(或者一个捕获的TaskScheduler,如果没有SynchronizationContext)上恢复。通常,这正是您想要的:异步控制器操作将等待某些内容,当它恢复时,它将与请求上下文一起恢复。
下面是test5失败的原因:
Test5Controller.Get executes AsyncAwait_GetSomeDataAsync (within the ASP.NET request context).
AsyncAwait_GetSomeDataAsync executes HttpClient.GetAsync (within the ASP.NET request context).
The HTTP request is sent out, and HttpClient.GetAsync returns an uncompleted Task.
AsyncAwait_GetSomeDataAsync awaits the Task; since it is not complete, AsyncAwait_GetSomeDataAsync returns an uncompleted Task.
Test5Controller.Get blocks the current thread until that Task completes.
The HTTP response comes in, and the Task returned by HttpClient.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 in Test5Controller.Get.
Deadlock.
以下是其他方法有效的原因:
(test1, test2,和test3): Continuations_GetSomeDataAsync调度线程池的延续,在ASP。NET请求上下文。这允许由Continuations_GetSomeDataAsync返回的任务完成,而不必重新进入请求上下文。
(test4和test6):由于任务正在等待,ASP。NET请求线程没有阻塞。这允许AsyncAwait_GetSomeDataAsync使用ASP。当它准备继续时,NET请求上下文。
以下是最佳实践:
在你的“库”异步方法中,尽可能使用ConfigureAwait(false)。在你的情况下,这将改变AsyncAwait_GetSomeDataAsync为var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
不要阻塞任务;它完全是异步的。换句话说,使用await而不是GetResult (Task。结果和任务。Wait也应该替换为await)。
这样,你可以得到两个好处:延续(AsyncAwait_GetSomeDataAsync方法的剩余部分)在基本线程池线程上运行,不需要进入ASP。NET请求上下文;而且控制器本身是异步的(它不会阻塞请求线程)。
更多信息:
我的async/await介绍帖子,其中包括任务等待器如何使用SynchronizationContext的简要描述。
Async/Await常见问题解答,其中详细介绍了上下文。还可以查看Await、UI和死锁!哦,我的天!这在这里也适用,即使你在ASP。NET而不是UI,因为ASP。NET SynchronizationContext将请求上下文限制为一次只有一个线程。
这是MSDN论坛的帖子。
Stephen Toub演示了这种死锁(使用一个UI), Lucian Wischik也演示了。
更新2012-07-13:将这个答案整合到一篇博客文章中。
编辑:一般来说,尽量避免做下面的事情,除非是为了避免死锁而做的最后努力。阅读Stephen Cleary的第一条评论。
从这里开始快速修复。而不是写:
Task tsk = AsyncOperation();
tsk.Wait();
Try:
Task.Run(() => AsyncOperation()).Wait();
或者如果你需要一个结果:
var result = Task.Run(() => AsyncOperation()).Result;
从源代码(经过编辑以匹配上面的示例):
AsyncOperation现在将在线程池上调用
将不会是一个SynchronizationContext,以及内部使用的延续
AsyncOperation不会被强制返回到调用线程。
对我来说,这看起来是一个有用的选项,因为我没有选择使它异步的所有方式(这是我更喜欢的)。
来源:
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().