[英]Encapsulating async method in Task.Factory.StartNew
我对Task和async / await模式有很好的理解,但是最近有人修复了他所说的由以下原因造成的死锁:
public async Task<HttpResponseMessage> GetHttpResponse()
{
using (var client = new HttpClient())
{
return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);
}
}
他说他用某种Task.Factory.StartNew
模式修复了它。
public HttpResponseMessage GetHttpResponse()
{
using (var client = new HttpClient())
{
return Task.Factory.StartNew(() => client.SendAsync(new HttpRequestMessage()).Result).Result;
}
}
首先,问题是我为什么要将return语句更改为HttpResponseMessage
而不是Task<HttpResponseMessage>
。
我的第二个问题是,为什么这段代码会解决死锁错误。 从我的理解,即他所说的事实.Result
迫使GetHttpResponse
线程等待(并冻结),直到client.SendAsync
完成。
任何人都可以尝试解释这个代码如何影响任何TaskScheduler
和SynchronizationContext
。
谢谢您的帮助。
编辑:这是调用方法,为问题提供更多的上下文
public IWebRequestResult ExecuteQuery(ITwitterQuery twitterQuery, ITwitterClientHandler handler = null)
{
HttpResponseMessage httpResponseMessage = null;
try
{
httpResponseMessage = _httpClientWebHelper.GetHttpResponse(twitterQuery, handler).Result;
var result = GetWebResultFromResponse(twitterQuery.QueryURL, httpResponseMessage);
if (!result.IsSuccessStatusCode)
{
throw _exceptionHandler.TryLogFailedWebRequestResult(result);
}
var stream = result.ResultStream;
if (stream != null)
{
var responseReader = new StreamReader(stream);
result.Response = responseReader.ReadLine();
}
return result;
}
// ...
用于修复死锁的修改代码使用Task APIs
非常糟糕,我可以看到很多问题:
Async call to Sync call
转换Async call to Sync call
,由于在Task
上使用Result,因此它是一个阻塞调用 Task.Factory.StartNew
,是一个很大的不,检查StartNew是由Stephen Cleary 危险的 关于您的原始代码,以下是最可能的deadlock
原因:
ConfigureAwait(false)
不起作用的原因是您需要在所有Async calls
中的完整调用堆栈中使用它,这就是它导致Deadlock
的原因。 Point是client.SendAsync
和完整的调用链需要有一个ConfigureAwait(false)
,以便忽略Synchronization context
。 只是在一个地方并没有帮助,并且不能保证不在您控制范围内的电话 可能解决方案
在这里简单的删除应该工作,你甚至不需要Task.WhenAll
,因为没有多个tasks
using (var client = new HttpClient()) { return await client.SendAsync(new HttpRequestMessage()); }
另一个不太优选的选择是:
使代码同步如下(现在整个链中不需要ConfigureAwait(false)
):
public HttpResponseMessage GetHttpResponse() { using (var client = new HttpClient()) { return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false).GetAwaiter().GetResult(); } }
死锁不是由return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);
引起的return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);
。 这是由于阻塞调用堆栈引起的。 因为它是非常不太可能的MS执行HttpClient.SendAsync()
具有一定的阻挡代码内(这可能死锁),它必须是在呼叫者之一到public async Task<HttpResponseMessage> GetHttpResponse()
使用.Wait()
或.Result
返回任务的.Result
。 你的同事所做的就是将阻塞调用移动到堆栈中,在那里它更加明显。 此外, 这个“修复”甚至不能解决传统的死锁,因为它使用相同的同步上下文(!)。 你可以在堆栈的其他地方推断出一些其他函数卸载没有asp.net同步上下文的新任务,并且在新的(可能是默认的)上下文中阻塞GetHttpResponse()
执行,否则“修复”也会出现死锁!
因为在现实世界中,并不总是可以将生成遗留代码重构为异步,所以您应该使用自己的异步HttpClient接口,并确保实现使用.ConfigureAwait(false)
,因为所有基础结构库都应该如此。
@ mrinal-kamboj @ panagiotis-kanavos @shay
感谢大家的帮助。 如上所述,我已经开始阅读Async in C# 5.0 from Alex Davies
的Async in C# 5.0 from Alex Davies
。
在第8章:哪个线程运行我的代码 ,他提到我们的情况,我想我在那里找到了一个有趣的解决方案:
您可以通过在启动异步代码之前移动到线程池来解决死锁问题,以便捕获的SynchronizationContext是线程池而不是UI线程。
var result = Task.Run(() => MethodAsync()).Result;
通过调用Task.Run
他实际上强制异步调用SynchronizationContext
成为ThreadPool的SynchronizationContext
(这是有意义的)。 通过这样做,他还确保代码MethodAsync
既不启动也不返回主线程。
考虑到这一点,我改变了我的代码如下:
public HttpResponseMessage GetHttpResponse()
{
using (var client = GetHttpClient())
{
return TaskEx.Run(() => client.SendAsync(new HttpRequestMessage())).Result;
}
}
此代码似乎适用于Console,WPF,WinRT和ASP.NET。 我会进一步测试并更新这篇文章。
在本书中,我了解到.ConfigureAwait(false)
仅阻止回调调用SynchronizationContext.Post()
方法在调用者Thread上运行。 要确定应该运行回调的线程, SynchronizationContext
检查与其关联的线程是否重要 。 如果是,那么它会选择另一个线程。
根据我的理解,这意味着回调可以在任何线程(UI-Thread或ThreadPool)上运行。 因此,它不保证UI-Thread上的不执行,但使其不太可能。
有趣的是,以下代码不起作用:
public async Task<HttpResponseMessage> GetHttpResponse()
{
using (var client = GetHttpClient())
{
return await TaskEx.Run(() => client.SendAsync(new HttpRequestMessage()));
当我尝试使用这段代码时,我记得.Result
可以在ThreadPool范围之外使用.Result
。 在某种程度上它对我有意义,但如果有人想对此发表评论,他将受到欢迎:)
我想加入ConfigureAwait
到您的注(2)代码将做它的工作。 ASP.NET上下文一次只允许1个线程在其中运行。 线程池上下文允许一次运行多个线程。
await将代码执行返回给调用方法。 调用方法在ASP.net上下文和.Result块的调用。 当client.SendAsync
返回时,它将完成ASP.net上下文中的方法,但不能,因为上下文中已经有一个线程阻塞(对于.Result)。
如果您将ConfigureAwait
与client.SendAsync
一起client.SendAsync
,它将能够在与ASP.Net上下文不同的上下文中完成该方法,并在调用方法中完成获取.Result的方法。
即.Result正在等待GetHttpResponse
方法的结束,而GetHttpResponse
方法的结束正在等待。 结果退出ASP.net上下文完成。
在你原来的例子中,当调用client.SendAsync
等待client.SendAsync
离开ASP.net上下文继续时,调用方法阻塞client.SendAsync
。 就像你说的那样, ConfigureAwait(false)
只会影响await之后从返回执行的代码的上下文。 Client.SendAsync
正等待进入ASP.NET上下文执行。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.