当前位置 博文首页 > .NET云原生:抓住异步编程async/await语法糖的牛鼻子: Synchroni

    .NET云原生:抓住异步编程async/await语法糖的牛鼻子: Synchroni

    作者:.NET云原生 时间:2021-05-27 18:23

    长话短说,本文带大家抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext

    引言

    C#异步编程语法糖async/await,使开发者很容易就能编写异步代码。
    零散看过很多文章,很多是填鸭式灌输 (有的翻译文还有偏差)。

    遵守以上冷冰冰的②③条的原则,可以确保我们的异步程序按照预期运作,但是我们常看到违背这2条原则引发的死锁现场。

    由async/await引起的死锁现场

    UI例子:

    点击按钮触发一个HTTP请求,用请求的返回值修改UI控件, 以下代码会引发deadlock (类似状态出现在WinForm、WPF)

    public static async Task<JObject> GetJsonAsync(Uri uri)
    {
      using (var client = new HttpClient())
      {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
      }
    }
    
    // 上层调用方法
    public void Button1_Click(...)
    {
      var jsonTask = GetJsonAsync(...);
      textBox1.Text = jsonTask.Result;
    }
    

    ASP.NET web例子:

    从api发起远程HTTP请求,等待请求的结果,以下代码也会引发deadlock

    public static async Task<JObject> GetJsonAsync(Uri uri)
    {
      using (var client = new HttpClient())
      {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
      }
    }
    // 上层调用方法
    public class MyController : ApiController
    {
      public string Get()
      {
        var jsonTask = GetJsonAsync(...);
        return jsonTask.Result.ToString();
      }
    }
    

    ?? 解决以上死锁有2种编程写法:

    1. 不再混用异步、同步写法, 始终使用async/await语法糖编写异步代码
    2. 在等待的异步任务时应用ConfigureAwait(false)方法

    SynchronizationContext就是解决这类死锁的牛鼻子,大多数时候SynchronizationContext是在异步编程后默默工作的,但是了解这个对象对于理解Task、await/sync 工作原理大有裨益。

    本文会解释:

    1. async/await工作机制
    2. SynchronizationContext在异步编程语法糖中的意义
    3. 为什么会有deadlock

    1. await/async语法糖工作机制

    微软提出了Task线程包装类和 await/async简化了异步编程的方式:

    第②步:调用异步方法GetStringAsync时,开启异步任务;

    第⑥步:遇到await关键字,框架会捕获调用线程的同步上下文(SynchronizationContext)对象,附加给异步任务;同时,控制权上交到上层调用函数;

    第⑦步:异步任务完成,通过IO完成端口通知上层线程,
    第⑧步:通过捕获的线程同步上下文执行后继代码块;

    2.SynchronizationContext的意义

    先看下MSDN中关于SynchronizationContext的定义:

    提供在各种同步模型中传播同步上下文的基本功能。此类实现的同步模型的目的是允许公共语言运行库的内部异步/同步操作使用不同的同步模型正常运行。

    ??这完全不是人能看懂的解释,我给出的解释是:在线程切换过程中保存调用线程的上下文, 用于在异步任务完成后使用此线程同步上下文执行后继代码

    这个线程同步上下文的意义在哪?

    我们大家都知道:WinForm和WPF都有类似的原则: 长耗时的任务在后台计算,将异步结果返回给UI线程

    这个时候我们就需要捕获UI线程的SynchronizationContext,并将这个对象传入后台线程。

    public static void DoWork()
    {
        //On UI thread
        var sc = SynchronizationContext.Current;
    
        ThreadPool.QueueUserWorkItem(delegate
        {
           // do work on ThreadPool
            sc.Post(delegate
            {
                 // do work on the original context (UI)
            }, null);
        });
    }
    

    SynchronizationContext标识了代码运行的线程环境,每个线程都有自己的SynchronizationContext,通过SynchronizationContext.Current可以获取当前线程的同步上下文。

    在异步线程切换场景中,使用SynchronizationContext ,就可以返回到调用线程。
    不同的.NET框架因各自独特的需求有不同SynchronizationContext子类(通常是重写父类虚方法):

    • ASP.NET有AspNetSynchronizationContext
    • Windows Form有WindowsFormSynchronizationContext
    • WPF 有DispatcherSynchronizationContext
    • ASP.NET Core、控制台程序不存在SynchronizationContext,SynchronizationContext.Current=null

    AspNetSynchronizationContext维护了HttpContext.Current、用户身份和文化,但在ASP. NET Core这些信息天然依赖注入,故不再需要SynchronizationContext;另一个好处是不再获取同步上下文对性能也是一种提升。
    因此,对于ASP.NET Core程序,ConfigureAwait(false)不是必需的,然而,在基础库时最好还是使用ConfigureAwait(false),因为你保不准上层会混用同步/异步代码。

    3.引言代码为什么发生deadlock

    观察引言代码,控制权返回到上层调用函数时,执行流使用Result/(Wait方法)等待任务结果,Result/Wait()会导致调用线程同步阻塞(等待任务完成), 而异步任务执行完成后,会尝试利用捕获的同步上下文执行后继代码,这样形成死锁。

    正因为如此,我们提出:

    • 在调用函数始终使用await方法,这样调用线程是异步等待任务完成,后继代码可以在该线程同步上下文上执行
    • 对异步任务应用ConfigureAwait(false)方法

    ConfigureAwait(bool):true 表示尝试在捕获的原调用线程SynchronizationContext 中执行后继代码;false 不再尝试在捕获的线程SynchronizationContext中执行后继代码。 ConfigureAwait(false) 能解决[因调用线程同步阻塞]引发的死锁,但是同步阻塞没有利用异步编程的优点,不是很推荐。

    你会看到,这两种缓解死锁的方案其实 都是针对SynchronizationContext
    ASP.NET Core和控制台程序,因为捕获的SynchronizationContext=null, 会选择一个线程同步上下文来执行,不会死锁。

    总结

    微软为加快开发效率上着实费了心力,.NET提供的await/async语法糖简化了异步编程方式,

    在异步编程中,SynchronizationContext决定了后继代码在哪里执行的环境,深入理解这个对象的背景和不同框架的实现方式,能帮助我们避免编写死锁代码。

    bk
    下一篇:没有了