看过这么多爆文,依旧走不好异步编程这条路?​

简介: 本文带大家抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext。

引言


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


a9aee06f6abcda8cd1bb6709ca2e10f1.png


遵守以上冷冰冰的②③条的原则,一般可确保异步程序按预期运作,


我们时常能在各大论坛看到同学们(因不遵守②③点)引发的死锁现场。


由async/await引起的死锁现场


UI程序(WinForm、WPF):点击按钮,触发一个HTTP请求,用请求结果修改UI控件,以下代码会引发deadlock


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


ASP.NET web程序:从api接口发起HTTP请求,返回请求的结果,以下代码也会引发deadlock


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


☺️ 解决以上死锁有两种编程方式:


  1. 不再混用异步/同步写法, 始终使用async/await语法糖编写异步代码


  1. 对等待的异步任务应用ConfigureAwait(false)方法


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


本文会解释:


  1. async/await工作机制


  1. SynchronizationContext在异步编程语法糖中的意义


  1. 示例代码为什么会deadlock


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


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


94467e36e2fe626b3fd158bec90cb6fe.png


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


第⑥步:遇到await关键字,框架会捕获调用线程的同步上下文

(SynchronizationContext)对象, 附加给异步任务;同时控制权上交到上层调用函数;


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


2. SynchronizationContext的意义


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


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


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


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


大家都知道:WinForm和WPF都有类似的原则:长耗时的任务在后台进行,将异步结果返回给UI线程 。(这难道就是ConfigureAwait方法默认传true的原因?)


此时就需要捕获UI线程的SynchronizationContext,并将这个对象传入异步任务。


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


SynchronizationContext表示代码运行的线程环境,在异步编程中,利用该对象切换代码执行环境。


不同的.NET框架因各自独特的线程切换场景有不同的SynchronizationContext子类(重写父类虚方法):


  • ASP.NET有AspNetSynchronizationContext


  • WinForm有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决定了后继代码在哪里执行的环境,深入理解这个对象的背景和不同框架的实现方式,能帮助我们避免编写死锁代码。

相关文章
|
4月前
|
设计模式 存储 算法
谈谈代码:如何避免写出糟糕if...else语句
在写代码的日常中,`if...else`语句是极为常见的.正因其常见性,很多同学在写代码的时候并不会去思考其在目前代码中的用法是否妥当.而随着项目的日渐发展,糟糕的`if...else`语句将会充斥在各处,让项目的可维护性急剧下降.故在这篇文章中,笔者想和大家谈谈如何避免写出糟糕`if...else`语句.
28 0
谈谈代码:如何避免写出糟糕if...else语句
|
6月前
|
测试技术
代码为啥不能过度优化
代码为啥不能过度优化
37 0
|
8月前
|
前端开发 JavaScript 开发工具
🎖️我只会使用前端框架开发不行吗?
我们经常看到一个现象:一些前端开发人员可能会在使用最新框架创建漂亮的网站的同时,却在编写基本的CSS样式或创建简单的JavaScript函数方面感到困难。
66 0
|
9月前
|
设计模式 IDE 测试技术
让你更高效的开发技巧,少加班多冒泡!
作为开发者,你是否希望提高工作效率,减少加班时间,还能与团队更好地合作?没问题!本文为你准备了实用的技巧。比如,选择适合你的IDE工具、合理规划任务、提前搞清楚需求、经常进行代码审查和重构等等。只要你掌握了这些技巧,你将能轻松应对开发挑战,成为一名高效而受欢迎的开发者!千万别错过哦!
54 0
涨姿势了!原来这才是多线程正确实现方式
线程同步机制是一套适用于协调线程之间的数据访问机制,该机制可以保障线程安全 java平台提供的线程同步机制包括:锁、volatile关键字、final关键字,static关键字、以及相关API如object.wait/object.notify
|
算法 C++
没什么。。。。
没什么。。。。
|
Java C语言
看似无害的代码如何搞垮系统
编程就像魔法。最近遇到一个诡异的问题:添加一段看似无害的简单代码后,系统原有功能不可用了。 ## 复现演示 jdk 8 可使用如下演示代码复现这个问题。 `TaskCenter` 是一个任务框架,可添加多个任务,随后框架将执行这些任务。 `First` 任务是新增代码,看起来简单无害,且看不出对原有任务 `Count` 有何影响,但添加 `First` 任务后,其自身执行正常,原本正常的 `C
|
前端开发 API
我明白了,前端并发函数
谷歌浏览器的并发数是6个,有六个请求正在处理,那么其它的任务就会排队,等六个请求任务中的某一个完成之后,就会立马插入进去执行。这样一来,就清楚了。
150 0
我明白了,前端并发函数
重生之我在人间敲代码_Java并发基础_用“等待-通知”机制优化循环等待
在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待。如果循环等待操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的,因为这种场景下,循环上几次或者几十次就能一次性获取转出账户和转入账户了。但是如果循环等待操作耗时长,或者并发冲突量大的时候,循环等待这种方案就不适用了,因为在这种场景下,可能要循环上万次才能获取到锁,太消耗 CPU 了。
|
算法 Java 调度
一篇文章,搞明白异步和多线程的区别
一篇文章,搞明白异步和多线程的区别
865 0
一篇文章,搞明白异步和多线程的区别