专栏首页全栈码农画像看过这么多爆文,依旧走不好异步编程这条路?​

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

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

引言

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

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

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

由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语法糖编写异步代码
  2. 对等待的异步任务应用ConfigureAwait(false)方法

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

本文会解释:

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

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

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

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

本文分享自微信公众号 - Dotnet Plus(nodotnet),作者:精益码农

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-05-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 智能硬件“双雄”:先跑的VR何故被智能音箱反超?

    【摘要】虚拟现实(VR)设备曾被众多科技巨头寄予厚望,如今增长势头却被智能音箱远远甩在后面。智能音箱行业2019年的表现,足以用炫目来形容。

    用户2908108
  • 智能硬件“双雄”:先跑的VR何故被智能音箱反超?

    【摘要】虚拟现实(VR)设备曾被众多科技巨头寄予厚望,如今增长势头却被智能音箱远远甩在后面。智能音箱行业2019年的表现,足以用炫目来形容。

    曾响铃
  • 传统电竞愈演愈热,那VR电竞呢?

    近年来,电竞游戏风靡全球。国内电竞市场也在不断的发展壮大,渐渐的,电竞市场也成为了全民性的竞赛项目。今年5月,亚奥理事会正式宣布将《英雄联盟》等6项电子竞技游戏...

    VRPinea
  • 2020的智能音箱,品牌们在靠什么“博弈”?

    在国内智能音箱赛道,历经几年的角逐下最终只留下了“三大玩家”,这是大家都知道的事实。在今年以前,“三强”前进的脚步也都趋近一致,发力家庭IoT场景、扩大硬件生态...

    曾响铃
  • 与时俱进 | 博客现已运行在 .NET Core 3.0 及 Azure 上

    9月23日,微软正式发布了 .NET Core 3.0,这个版本具有大量新功能和改进。我也在第一时间将自己的博客网站更新到了 .NET Core 3.0,并且仍...

    Edi Wang
  • 技术分析 | HarmonyOS到底是不是Android套皮?

    最近鸿蒙系统关注度好高,支持与反对、看好和看衰、「自主的全场景分布式系统」和「Android套壳」各执一词,吵的不可开交。

    刘盼
  • 优信二手车:赛道虽好,生意难做

    高营销投入没能换来高回报,优信、瓜子二手车、人人车等二手车电商,集体陷入求生困境。

    刘旷
  • 「能写代码」是愚公移山,「会写代码」是女娲补天

    ? 导语:你知道普通程序员和优秀程序员之间的差距吗?其实答案很简单,那就是「愚公移山」和「女娲补天」之间的区别。 之所以提这个话题,跟前两天在微信群里的讨论有...

    非著名程序员
  • 周立功: 走在人生边缘

    每天匆匆忙忙,看似很充实,到头来却发现,其实人最愧对的是自己,甚至连一顿早餐都没吃好。

    Linux阅码场

扫码关注云+社区

领取腾讯云代金券