引言:

  多线程编程/异步编程非常复杂,有很多概念和工具需要去学习,贴心的.NET提供Task线程包装类await/async异步编程语法糖简化了异步编程方式。

相信很多开发者都看到如下异步编程实践原则:

  实践原则  说明  例外情况
 ①  避免 Async Void  最好使用 async Task 方法而不是 async void 方法  事件处理程序
 ②  始终使用 await  不要混合阻塞式代码和异步代码  控制台 main 方法
 ③  配置上下文  尽可能使用ConfigureAwait(false)  需要上下文的方法
  
  遵守以上冷冰冰的②③条的原则,可保证异步程序按照预期状态正常运作;我们在各大编程论坛常看到违背这2条原则引发的莫民奇妙的死锁问题。

  UI 例子:点击按钮触发了一个远程HTTP请求,用请求的返回值修改UI控件, 以下代码会引发deadlock (类似状态出现在Windows Form、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例子:API Action发起远程HTTP请求,等待请求的json结果,并解析json字符串,以下代码也会引发deadlock
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}
// My "top-level" method.
public class MyController : ApiController
{
  public string Get()
  {
    var jsonTask = GetJsonAsync(...);
    return jsonTask.Result.ToString();
  }
}
   解决以上deadlock需利用以上第②③条编程原则:
  • 不要混合使用异步、同步代码,始终使用async/await语法糖编写异步代码

  • 在等待的异步任务内应用ConfigureAwait(false)方法 (:不再尝试从捕获的同步上下文执行异步编程的后续代码)

   第②③条原则与我们今天的主角SynchronizationContext 密切相关,大多数时候SynchronizationContext 是在异步编程后面默默工作的, 但是了解这个对象对于理解Task、await/sync 工作原理大有裨益。本文会解释
  • 为什么要有SynchronizationContext 对象

  • 阐述await关键字与SynchronizationContext对象交互原理

  • 以上代码为什么会有deadlock, 另外ASP.NET Core为什么不会发生以上死锁

1. The Need for SynchronizationContext

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

  上面的定义给我的印象是:在线程切换过程中保存前置线程执行的上下文环境。

  我们大家都知道:Windows Form和WPF都基于类似的原则: 不允许在非UI线程上操作 UI元素

   

  这个时候我们可以捕获当前执行环境SynchronizationContext,利用这个对象切换回原UI线程。

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子类(通常是重写Post虚方法):

  - 默认SynchronizationContext封装的是线程池内线程,将执行委托发送到线程池中任意线程。

  - asp.Net有AspNetSynchronizationContext,在一个异步page处理过程中,context始终使用的是线程池中某个特定线程

  - Windows Form有WindowsFormSynchronizationContext,封装单个UI线程,Post方法将委托传递给 Control.BeginInvoke

  - WPF 有DispatcherSynchronizationContext, 了解到与WinForm 类似。

2. await/async语法糖与SynchronizationContext 的关系?

  以上ThreadPool.QueueUserWorkItem 涉及线程底层,微软提出Task线程包装类和 await/async 简化了异步编程的方式:
 
  
  ① 调用异步方法GetStringAsync时,.NET框架为我们创建了异步任务T;

  ② 应用await时,框架捕获当前环境, 存储在SynchronizationContext 对象并附加于以上Task;

  ③ 同时,控制权返回到原上层调用函数,返回一个未完成的Task<int>对象,这个时候需要关注上层调用函数使用 await异步等待还是使用Result/Wait()方式同步等待

  ④ 异步任务T执行完成,await之后的代码将会成为continuation block, 默认情况下利用捕获的SynchronizationContext 对象执行该continuation block 代码。

    内部实际是将continuation block代码放入SynchronizationContext 的Post方法。

    

3.引言代码为什么发生deadlock, 而ASP.NET Core/控制台程序为什么不会发生类似deadlock?

仔细观察引言代码,控制返回到 上层调用函数时, 该调用函数使用Result属性去等待任务结果,Result/Wait()等同步方式会导致调用线程挂起等待任务完成。而在异步方法内部,await触发的异步任务执行完成后,会尝试利用捕获的同步上下文执行剩余代码,而该同步上下文中的线程正同步等待整个异步任务完成,形成死锁。

正因为如此,我们提出:

  - 在原调用函数始终 使用 await方法,这样该线程是异步等待 任务完成。

  - 在异步任务内部应用ConfigureAwait(false)方法, 不尝试使用捕获的同步上下文执行后继代码

MSDN ConfigureAwait(): true to attempt to marshal the continuation back to the original context captured; otherwise, false

  另外注意:ASP.NET Core,,控制台程序不存在SynchronizationContext , 故不会发生类似的死锁。

 

 总结:

  虽然await/async 语法糖让我们在编写.NET 异步程序时得心应手、随心所欲,但是不要忘记了SynchronizationContext 在其中转承起合的作用。

利用能够保存当前执行代码的上下文特性,SynchronizationContext在线程切换后帮我们有能力执行各种骚操作。

作者:Julian_酱

感谢您的认真阅读,如有问题请大胆斧正,如果您觉得本文对你有用,不妨右下角点个或加关注。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置注明本文的作者及原文链接,否则保留追究法律责任的权利。

 

【.NET异步编程系列2】掌控SynchronizationContext避免deadlock的更多相关文章

  1. 【异步编程】Part2:掌控SynchronizationContext避免deadlock

    引言: 多线程编程/异步编程非常复杂,有很多概念和工具需要去学习,贴心的.NET提供Task线程包装类和await/async异步编程语法糖简化了异步编程方式. 相信很多开发者都看到如下异步编程实践原 ...

  2. 【.NET异步编程系列1】:await&async语法糖让异步编程如鱼得水

    前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...

  3. 异步编程系列第02章 你有什么理由使用Async异步编程

    p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...

  4. 异步编程系列第01章 Async异步编程简介

    p { display: block; margin: 3px 0 0 0; } --> 2016.10.11补充 三个月过去了,回头来看,我不得不承认这是一系列失败的翻译.过段时间,我将重新翻 ...

  5. 异步编程系列第04章 编写Async方法

    p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...

  6. 异步编程系列第05章 Await究竟做了什么?

    p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...

  7. 异步编程系列06章 以Task为基础的异步模式(TAP)

    p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...

  8. 【.NET异步编程系列3】取消异步操作

    在.Net和C#中运行异步代码相当简单,因为我们有时候需要取消正在进行的异步操作,通过本文,可以掌握 通过CancellationToken取消任务(包括non-cancellable任务).  早期 ...

  9. 【异步编程】Part1:await&async语法糖让异步编程如鱼得水

    前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...

随机推荐

  1. 日常踩坑笔记:spring的context:property-placeholder标签

    背景: 原来的项目一直跑着没有问题,今天突然想在原有项目的基础上,加上redis进行数据的缓存,原来项目的架构就是传统的SSM框架,于是,大刀阔斧的开始改装了... 编写redis的配置文件——red ...

  2. 凸包问题——Graham Scan

    Graham Scan 概述: 对于凸多边形的定义不在这里做详细叙述,这里给出算法的实现原理. Step 1: 找出x值最小的点的集合,从其中找出y值最小的点作为初始点 Step 2: 获得新序列后, ...

  3. margin-right没有效果的问题

    margin-right其实有效果的,只是在默认即标准流的情况的下显示不出来效果.如果脱离标准流呢?想到这个,就立马在css文件中加了一个:float:right;然后在测试的时候就能看到margin ...

  4. 软件及博客的markdown支持度的评测

    软件 vscode vscode原生支持markdown,但对数学公式的支持不太好,用 $$包含的数学公式不支持换行,而且在数学公式里面不能输入中文 Typora 非常简洁优美的软件,只有预览页,没有 ...

  5. Python_正则表达式二

    ''' 正则表达式对象的sub(repl,string[,count=0])和subn(repl,string[,count=0])方法用来实现字符串替换功能 ''' example='''Beaut ...

  6. hexo+github创建属于自己的博客

    配置环境 安装Node(必须) 作用:用来生成静态页面的 到Node.js官网下载相应平台的最新版本,一路安装即可. 安装Git(必须) 作用:把本地的hexo内容提交到github上去. 安装Xco ...

  7. Eclipse开发前,常用设置

    设置工作空间的项目编码, 防止出现乱码    Window - Preferences - General - Workspace    将"Text file encoding" ...

  8. SSH整合配置文件概括

    配置方式一:struts.xml, applicationContext.xml(hibernate.cfg.xml配置信息写入spring配置文件中) (版本号, struts2:2.3.15; s ...

  9. 数据结构 之 二叉堆(Heap)

    注:本节主要讨论最大堆(最小堆同理). 一.堆的概念     堆,又称二叉堆.同二叉查找树一样,堆也有两个性质,即结构性和堆序性.     1.结构性质:     堆是一棵被完全填满的二叉树,有可能的 ...

  10. mybatis整合spring获取配置文件信息出错

    描述:mybatis整合spring加载jdbc.properties文件,然后使用里面配置的值来 配置数据源,后来发现用户变成了admin- jdbc.properties的配置: 加载配置: 报错 ...