9

抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext

 3 years ago
source link: https://www.cnblogs.com/JulianHuang/p/14817621.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

长话短说,本文带大家抓住异步编程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决定了后继代码在哪里执行的环境,深入理解这个对象的背景和不同框架的实现方式,能帮助我们避免编写死锁代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK