封装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完成。

谁能告诉我这段代码是如何影响TaskSchedulerSynchronizationContext的?

谢谢你的帮助。

编辑:这里是调用方方法,为问题提供更多上下文

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.Factory.StartNew中的异步方法

死锁不是由return await client.SendAsync(new HttpRequestMessage()).ConfigureAwait(false);引起的。这是由于堆栈的阻塞调用引起的。由于HttpClient.SendAsync()的MS实现非常不太可能在内部有一些阻塞代码(这可能会死锁),因此必须是在返回的Task上使用.Wait().Resultpublic async Task<HttpResponseMessage> GetHttpResponse()的调用者之一。你的同事所做的只是将阻塞调用移动到堆栈中更显眼的地方。此外,这个"修复"甚至不能解决经典的死锁,因为它使用相同的同步上下文(!)。你可以推断,在堆栈的其他地方,一些其他函数卸载了一个没有asp.net同步上下文的新任务,并且在这个新的(可能是默认的)上下文中,你的阻塞GetHttpResponse()执行,否则"修复"也会死锁!

由于在现实世界中,不可能将生产遗留代码重构为所有方式的异步,因此您应该使用自己的async HttpClient接口,并确保实现使用.ConfigureAwait(false),因为所有基础设施库都应该这样做。

修改代码修复死锁是Task APIs的非常糟糕的使用,我可以看到很多问题:

  1. 它已经转换了Async call to Sync call,这是一个阻塞调用,由于使用Task上的Result
  2. 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)。

如果您使用ConfigureAwaitclient.SendAsync,它将能够在不同于ASP的上下文中完成方法。. Net上下文,并在调用方法中完成获取. result的方法。

即. result等待GetHttpResponse方法的结束,而GetHttpResponse方法的结束正在等待。结果退出ASP.net上下文以完成。

在你原来的例子中,它是。result在调用方法阻塞,而client.SendAsync正在等待。result从ASP.net上下文中出来继续。就像你说的,ConfigureAwait(false)只影响从await返回后执行的代码的上下文。而Client.SendAsync正在等待进入ASP。