在Task.Factory.StartNew中封装异步方法

时间:2016-09-12 00:07:49

标签: c# asynchronous async-await task-parallel-library

我对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完成。

任何人都可以尝试向我解释此代码对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;
    }
// ...

3 个答案:

答案 0 :(得分:1)

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

  1. 已转换Async call to Sync call,由于在Task
  2. 上使用了结果,这是一个阻止通话
  3. Task.Factory.StartNew,是一个很大的问题,请 Stephen Cleary检查StartNew is Dangerous
  4. 关于您的原始代码,以下是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();
        }
      }
      

答案 1 :(得分:1)

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

因为在现实世界中,并不总是可以将生产遗留代码重构为异步,所以您应该使用自己的异步HttpClient接口,并确保实现使用ControlToAdd作为所有基础结构图书馆应该。

答案 2 :(得分:0)

@ 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;
    }
}

此代码似乎适用于Console,WPF,WinRT和ASP.NET。我将进一步测试并更新这篇文章。

问题

  • 您认为这个新代码有意义吗?
  • 你认为它会阻止任何潜在的僵局吗?

在本书中,我了解到.ConfigureAwait(false)仅阻止回调调用SynchronizationContext.Post()方法在调用者线程上运行。要确定应该运行回调的线程,SynchronizationContext检查与其关联的线程是否为 important 。如果是,则选择另一个线程。

根据我的理解,这意味着回调可以在任何线程(UI-Thread或ThreadPool)上运行。因此,它不保证在UI-Thread上不执行,但使其不太可能。

注(2)

有趣的是,以下代码不起作用:

public async Task<HttpResponseMessage> GetHttpResponse()
{
    using (var client = GetHttpClient())
    {
        return await TaskEx.Run(() => client.SendAsync(new HttpRequestMessage()));

当我尝试使用此代码时,我想到.Result可以在等待.Result的ThreadPool范围之外使用。在某种程度上,这对我来说是有道理的,但如果有人想对此发表评论,他将受到欢迎:)