封装Task.Factory.StartNew中的异步方法
本文关键字:异步方法 StartNew Task Factory 封装 | 更新日期: 2023-09-27 18:06:45
我对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;
}
}
第一个问题是为什么他要把返回语句改为HttpResponseMessage
而不是Task<HttpResponseMessage>
。
我的第二个问题是,为什么这段代码可以解决死锁bug。从我的理解来看,他调用.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;
}
// ...
死锁不是由return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);
引起的。这是由于堆栈的阻塞调用引起的。由于HttpClient.SendAsync()
的MS实现非常不太可能在内部有一些阻塞代码(这可能会死锁),因此必须是在返回的Task上使用.Wait()
或.Result
的public async Task<HttpResponseMessage> GetHttpResponse()
的调用者之一。你的同事所做的只是将阻塞调用移动到堆栈中更显眼的地方。此外,这个"修复"甚至不能解决经典的死锁,因为它使用相同的同步上下文(!)。你可以推断,在堆栈的其他地方,一些其他函数卸载了一个没有asp.net同步上下文的新任务,并且在这个新的(可能是默认的)上下文中,你的阻塞GetHttpResponse()
执行,否则"修复"也会死锁!
由于在现实世界中,不可能将生产遗留代码重构为所有方式的异步,因此您应该使用自己的async HttpClient接口,并确保实现使用.ConfigureAwait(false)
,因为所有基础设施库都应该这样做。
修改代码修复死锁是Task APIs
的非常糟糕的使用,我可以看到很多问题:
- 它已经转换了
Async call to Sync call
,这是一个阻塞调用,由于使用Task
上的Result -
Task.Factory.StartNew
,是一个大的No,检查StartNew是危险的Stephen Cleary
关于你的原始代码,以下是deadlock
最可能的原因:
-
ConfigureAwait(false)
不起作用的原因是你需要在所有Async calls
的完整调用堆栈中使用它,这就是为什么它会导致Deadlock
。点是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(); } }
@mrinal-kamboj @panagiotis-kanavos @shay
谢谢你们所有人的帮助。如前所述,我已经开始阅读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;
}
}
这段代码似乎可以正常工作在控制台,WPF, WinRT和ASP.NET。我将进行进一步的测试并更新这篇文章。
<标题>- 你认为这个新代码有意义吗?
- 你认为它会防止任何潜在的死锁吗?
注意
在书中,我了解到.ConfigureAwait(false)
只阻止回调调用SynchronizationContext.Post()
方法在调用者线程上运行。为了确定回调应该在哪个线程上运行,SynchronizationContext
检查它所关联的线程是否重要。如果是,则选择另一个线程。
从我的理解,这意味着回调可以在任何线程(UI-Thread或ThreadPool)上运行。因此,它不能保证在ui线程上不执行,但使它非常不可能。
注(2)
有趣的是,下面的代码不起作用:public async Task<HttpResponseMessage> GetHttpResponse()
{
using (var client = GetHttpClient())
{
return await TaskEx.Run(() => client.SendAsync(new HttpRequestMessage()));
当我试图拥有这段代码时,我想到.Result
可以在ThreadPool等待.Result
的范围之外使用。在某种程度上,这对我来说是有意义的,但如果有人想对此发表评论,他将受到欢迎:)
我认为在你的Note(2)代码中添加ConfigureAwait
将使其工作。ASP。. NET上下文一次只允许1个线程在其中运行。线程池上下文允许同时运行1个以上的线程。
await将代码执行返回给调用方法。调用方法位于ASP.net上下文中,对. result的调用块。当client.SendAsync
返回时,它去完成ASP.net上下文中的方法,但不能,因为在上下文阻塞中已经有一个线程(对于. result)。
如果您使用ConfigureAwait
和client.SendAsync
,它将能够在不同于ASP的上下文中完成方法。. Net上下文,并在调用方法中完成获取. result的方法。
即. result等待GetHttpResponse
方法的结束,而GetHttpResponse
方法的结束正在等待。结果退出ASP.net上下文以完成。
在你原来的例子中,它是。result在调用方法阻塞,而client.SendAsync
正在等待。result从ASP.net上下文中出来继续。就像你说的,ConfigureAwait(false)
只影响从await返回后执行的代码的上下文。而Client.SendAsync
正在等待进入ASP。