zoukankan      html  css  js  c++  java
  • [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁

        这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁。内容主要是从作者Stephen Cleary的两篇博文中翻译过来.

    原文1Don'tBlock on Async Code

    原文2why the AspNetSynchronizationContext was removed

    示例代码:async_await中使用阻塞式代码导致死锁.rar

     

    一、async/await 异步代码运行流程

             async/await是在.NET4.5版本引入的关键字,让开发者可以更加轻松的创建异步方法。

             我们从下图来认识async/await的运行流程:

    二、在异步代码中阻塞,导致死锁的示例

    UI 示例

             单击一个按钮,将发起一个REST远程请求并且将结果显示到textbox控件上。(这是一个Windows Forms程序,同样也适用于其他任何UI应用程序)

    // My "library" method.

    publicstaticasync Task<JObject> GetJsonAsync(Uri uri)

    {

      using (var client = new HttpClient())

      {

        var jsonString = await client.GetStringAsync(uri);

        return JObject.Parse(jsonString);

      }

    }

     

    // My "top-level" method.

    publicvoid Button1_Click(...)

    {

      var jsonTask = GetJsonAsync(...);

      textBox1.Text = jsonTask.Result;

    }

             类库方法GetJsonAsync发起REST远程请求并且将结果解析为JSON返回。Button1_Click方法调用Task .Result阻塞等待GetJsonAsync处理完毕并显示结果

             这段代码会死锁。

    ASP.NET 示例

             在类库方法GetJsonAsync中发起一个REST远程请求,这次这个GetJsonAsyncASP.NET context中被调用。(示例是Web API项目,同样适用于任何一个ASP.NET应用程序 - 注:非ASP.NET Core应用)

    // My "library" method.

    publicstaticasync Task<JObject> GetJsonAsync(Uri uri)

    {

      using (var client = new HttpClient())

      {

        var jsonString = await client.GetStringAsync(uri);

        return JObject.Parse(jsonString);

      }

    }

     

    // My "top-level" method.

    publicclassMyController : ApiController

    {

      publicstring Get()

      {

        var jsonTask = GetJsonAsync(...);

        return jsonTask.Result.ToString();

      }

    }

             这段代码也会死锁。与UI示例是同一个原因。

     

    三、是什么原因导致的死锁呢?

             await一个Task后,在恢复继续执行时,会试图进入await之前的context

             第一个示例中,这个contextUI context(任何UI应用,除了控制台应用)。在第二个示例中,这个contextASP.NET request context

             另一个需要注意的点:ASP.NET request context 没有绑定到特定的线程上(像UI context一样),但是request context同一时刻只允许被绑定到一个线程上。我曾经在MSDN上有发表过关于SynchronzationContext的文章.

             死锁是怎么发生的呢?我们从top-level方法开始(UIButton1_Click方法或ASP.NETMyContoller.Get方法)

    1.         top-level方法调用GetJsonAsync(在UI/ASP.NET context中)。

    2.         GetJsonAsync通过HttpClient.GetStringAsync发起REST远程请求(在UI/ASP.NET context中)。

    3.         GetStringAsync返回一个未完成Task,标识REST远程请求还未处理完

    4.         GetJsonAsync方法中await GetStringAsync返回的未完成Task。等Task执行完毕,

    会重新捕获等待之前的context并使用它继续执行GetJsonAsync

    5.         GetJsonAsyncawait后,携带context的线程会跳出GetJsonAsync方法,继续执行后面的代码。并在jsonTask.Result发生阻塞。此时携带context的线程被阻塞了。

    6.         最终,REST请求处理完,GetStringAsync返回的Task处理完成。

    7.         GetJsonAsync方法准备继续运行,并且等待context可用,以便在context中执行。

    8.         发生context死锁。在top-level方法中已经阻塞了携带context的线程,等待GetJsonAsync返回的Task完成。而此时,GetJsonAsync方法正在等待context被释放,以便在context中继续执行。

     

    四、防止死锁

             这里有三个最佳实践来避免这种死锁(更详细传送门)。

    1.         在你的”library”异步方法中,返回未完成Task时都调用ConfigureAwait(false)

    2.         始终使用 Async不要混合阻塞式代码和异步代码

    3.         ASP.NET 升级为ASP.NET Core。在ASP.NET Core框架中,已经移除SynchronizationContext

    按照第一条最佳实践,”library”中的异步方法修改如下:

    publicstaticasync Task<JObject> GetJsonAsync(Uri uri)

    {

      using (var client = new HttpClient())

      {

        var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);

        return JObject.Parse(jsonString);

      }

    }

    ConfigureAwait(continueOnCapturedContext: false)continueOnCapturedContext参数表示是否尝试将延续任务封送回原始上下文。

    ConfigureAwait(false)改变了GetJsonAsync的延续行为,使它不用在原来的context中恢复。GetJsonAsync将直接在线程池线程中恢复,这使得GetJsonAsync能完成任务,并且无需重新进入原来的context

     

             按照第二条最佳实践。修改”top-level”方法如下:

    publicasyncvoid Button1_Click(...)

    {

      var json = await GetJsonAsync(...);

      textBox1.Text = json;

    }

     

    publicclassMyController : ApiController

    {

      publicasync Task<string> Get()

      {

        var json = await GetJsonAsync(...);

        return json.ToString();

      }

    }

             这样修改,改变了top-level方法的阻塞行为。所有的等待都是异步等待,这样context就不会被阻塞。

    其他异步等待指导原则:

    执行以下操作

    不要使用以下方式

    使用以下方式

    创建Task

    Task constructor

    Task.Run or TaskFactory.StartNew

    检索后台任务的结果

    Task.Wait Task.Result

    await

    等待任何任务完成

    Task.WaitAny

    await Task.WhenAny

    检索多个任务的结果

    Task.WaitAll

    await Task.WhenAll

    等待一段时间

    Thread.Sleep

    await Task.Delay

     

    五、在ASP.NET Core框架中,已经移除SynchronizationContext

    为什么AspNetSynchronizationContextASP.NET Core中被移除。尽管我不知道ASP.NET团队内部是什么观点,但我认为的观点是两个:性能和简单。

    性能方面:

    在没有SynchronizationContextASP.NET Core中,当一个async/await异步处理恢复执行时,会从线程池中获取一个线程并且执行继续操作。

    1.         避免了把操作排队到request context队列(request context同一时刻只允许被绑定到一个线程上)

    2.         避免了因为携带 request context 的线程被阻塞而发生“死锁”

    3.         不需要重新进入request context(重新进入request context涉及到很多内部作业任务,例如:设置HttpContext.Current和当前线程的身份标识(identity)和语言(culture))

    简单化:

    旧版本ASP.NETSynchronizationContext工作的很好,但是也存在棘手的问题,特别是在身份管理方面(参考:Thread.CurrentPrincipal VS HttpContext.Current.User)

     

    六、async/await避免阻塞式死锁最佳实践

    1.         在你的“library”异步方法中,返回未完成Task时都调用ConfigureAwait(false),标识不需要将延续任务封送回原始上下文。

    尽管在ASP.NET Core中,不用再调用ConfigureAwait(false)来防止死锁。但由于你提供的“类库”可能被用于UI 应用(egwinformwpf等)、旧版本ASP.NET应用、其他还存在context的应用。

    2.         更好的解决方案是“始终使用 async,不要混合阻塞式代码和异步代码”。

    因为当你在异步代码中阻塞程序,将失去异步代码带来的所有好处。并且异步代码释放当前线程带来的伸缩性也会失效。

    3.         ASP.NET项目升级为ASP.NET Core项目

     

    推荐阅读:

       Async/Await FAQ原文##译文

       Stephen Cleary 的开源组件:AsyncEx

            Async/Await 异步编程中的最佳做法(原文##译文

            (xishuai)ASP.NET混合阻塞式代码和异步代码,线程切换变化

     

  • 相关阅读:
    Nim or not Nim? hdu3032 SG值打表找规律
    Maximum 贪心
    The Super Powers
    LCM Cardinality 暴力
    Longge's problem poj2480 欧拉函数,gcd
    GCD hdu2588
    Perfect Pth Powers poj1730
    6656 Watching the Kangaroo
    yield 小用
    wpf DropDownButton 源码
  • 原文地址:https://www.cnblogs.com/owenzh/p/11023792.html
Copyright © 2011-2022 走看看