【C#异步】异步多线程的本质,上下文流转和同步
引言
net同僚对于async和await的话题真的是经久不衰,这段时间又看到了关于这方面的讨论,最终也没有得出什么结论,其实要弄懂这个东西,并没有那么复杂,简单的从本质上来讲,就是一句话,async 和await异步的本质就是状态机+线程环境上下文的流转,由状态机向前推进执行,上下文进行环境切换,在状态机向前推进的时候第一次的movenext会将当前线程的环境上下文保存起来,然后由TaskScheduler调度是否去线程池拿新线程执行这个task,等到后续推进到最后的movenext的时候,里面设置好结果,异常之后,回调则需要运行在调用await之前的环境上下文中去,这里说的是环境上下文,而并非是线程,所以当前环境上下文在await之前是A线程的上下文,在遇到await结束之后可能是B线程的环境上下文,并且异步是异步,线程是线程,异步不一定多线程,这两个不是等价的,针对async和await的源码刨析可以看一下之前写的博客https://www.cnblogs.com/1996-Chinese-Chen/p/15594498.html,这篇文章针对源码讲了一部分,可能不是很明了,只讲了async await执行的一个顺序对于环境上下文没有过多的描述,接下来,我会讲一些环境上下文,同步上下文的知识,以及在cs程序中,框架对于同步上下文的封装。
环境上下文ExecutionContext
ExecutionContext表示管理当前线程的执行上下文。针对此类,官网的解释是该 ExecutionContext 类为与逻辑执行线程相关的所有信息提供单个容器。 在.NET Framework中,这包括安全上下文、调用上下文和同步上下文。 在 .NET Core 中,不支持安全上下文和调用上下文,但是,模拟上下文和区域性通常通过执行上下文流动。 简单来说,这个类就是存放当前线程所有环境信息的容器,在net framework 和net core中,略有不同,后者不包括同步上下文,关于同步上下文和ExecutionContext,可以看看官网的另一篇比较好的文章https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/ 这篇文章,对于async await异步和上下文做了更加详细的解释。
那么在刚开始我们说了异步的本质之一就是上下文流转,那么什么是流转呢,怎么流转,这个类代表的存放当前线程信息的容器,那我们复制一份这个容器,然后放到另一个线程去,那另一个线程就可以获取到我们上一个线程内部的所有的信息,简单理解就是,搬家的时候我把我的所有东西打包放在我的新房子,那这个新房子也有了我搬家之前的那些信息,这个就是上下文流转,接下来,我们看一下实际在代码中的例子
public class TestTask
{
public static AsyncLocal<int> id;
}
private async void button1_Click(object sender, EventArgs e)
{
//var sss = new MyTask(() => { Console.WriteLine(111); });
//await sss;
var exce = ExecutionContext.IsFlowSuppressed();
TestTask.id = new AsyncLocal<int>() { Value = 1 };
// var asynclo=ExecutionContext.SuppressFlow();
var con1 = ExecutionContext.Capture();
var a = ExecutionContext.SuppressFlow();
exce = ExecutionContext.IsFlowSuppressed();
await Task.Delay(1000);
var con2 = ExecutionContext.Capture();
ExecutionContext.Run(con2, s =>
{
var sss = TestTask.id.Value;
}, null);
await Task.Delay(1000);
ExecutionContext.Restore(con1);
var sssa = TestTask.id.Value;
}
在上面的代码中,我首先定义了一个AsyncLocal 存放int类型的一个变量,在winform中我界面添加一个按钮,在点击事件中写下了如下代码,在第一行代码中调用了ExecutionContext.IsFlowSuppressed方法,这个方法是判断是否停止当前上下文的流转,
在刚开始运行的时候,这个返回结果是False,说明我们没有停止流转,是可以正常流转,在第二行代码中,我们给AsyncLocal变量赋值,设置Value为1;第三行中,我们使用了ExecutionContext.Capture方法,这个方法是捕获当前上下文信息,然后赋值给了con1变量,在往下走,我们调用了SuppressFlow方法,这个方法是我们阻止了当前上下文的流转,也就是说这个上下文是和await之后的上下文是不一样的,然后我们在判断IsFlowSuppressed的时候返回的就是true了,停止了流转,然后我们异步Delay1秒,然后我们捕获异步之后的当前线程的上下文信息,然后在这里我们捕获我们这个线程的上下文信息,接下来调用了ExecutionContext.Run方法,这个方法是将第二个参数的委托代码,运行在指定的上下文中去,这块这个run方法我们用不用其实都不影响演示效果,在这代码中,我们获取到id.Value就和上面的不一样获取的是默认值0,而不是上面定义的1,这就是因为我们停止了上下文流转,导致await前后不是同一个上下文,所以获取不到这个Value,如果我们不调用SuppressFlow,那在await之后就是上一个的上下文信息,获取到的Value也就是原来的1,在往下走,我们在Delay一下,在调用Restore方法,这个方法是将当前线程的上下文替换为指定的上下文信息,将指定上下文信息还原到当前线程,然后在获取的Value就是1了。
在ExectuionContext方法中有几个方法,就是Capture这个是静态捕获当前上下文信息,CreateCopy这个是实例方法,返回当前上下文信息的副本,IsFlowSuppressFlow判断是否停止上下文流转,SuppressFlow是停止上下文流转,Restore是将捕获的上下文信息还原到当前线程,当然了还有一个方法,和SuppressFlow方法对应,一个停止一个是恢复,叫RestoreFlow回复当前上下文在异步线程之间的流动,但是呢在async这个场景中是不适合这种情况的,是有一个报错,这个报错是当前上下文并没有停止上下文流转,这个是为什么呢,且听我娓娓道来。
我们都知道,线程的发展是Thread,Threadpool,再到现在的Task,然后Task是基于Threadpool封装的,那么我们在使用await Task之后的线程,是由Threadpool指定的,那他指定的线程不一定是await前的线程,就导致了你await之后恢复上下文流动的时候提示你上下文并没有停止流动,因为线程不一样导致的这个问题,就是说你SuppressFlow是另一个线程,await之后的是另一个线程,你RestoreFlow另一个线程,那肯定会报错啊,所以我们是需要使用Restore方法,将我们之前捕获的上下文信息还原到当前线程,这样,我们后续在获取Value的时候就可以获取到结果了。
这块还需要讲解一个问题就是,在上一段中,我们说了,Task的线程都是由Threadpool分配的,就会导致某些代码执行的线程是由Threadpool分配,那这个问题就导致了原有的Thread方面的东西是不能做线程数据传递的,例如,ThreadLocal,ThreadStaticAttribute特性,这些都是不能玩了的,因为使用Task,Threadpool的线程,我们不知道前后是否一样,那ThreadLocal和ThreadStatic 每一个线程是每一个线程的数据我们就会获取不到,这一点,大家在使用的时候还需要了解到。
SynchronizationContext
上面讲的ExecutionContext可以叫是线程环境上下文,SynchronizationContext提供在各种同步模型中传播同步上下文的基本功能。可以称它为线程同步上下文。如果ExectuionContext是整个环境信息的容器,那这个类是暴露给你整个环境信息的接口,虽然Execution也可以做不同线程之间的同步,但是你把所有的都暴露那总归是不好的,你能把你家的东西都让他知道吗,很显然不能,这个SynchronizationContext每个线程都可以设置自己的同步上下文信息,可以重写这个类,也可以就使用这个类去进行异步或者同步的分派信息到某个线程的上下文中去,同步使用Send方法,传入SendOrPostCallBack委托和委托需要的参数。
如果我们在线程中获取SynchronizationContext.Current的时候为空,null,我们可以创建一个SynchronizationContext的变量,var context=new SynchronizationContext();然后调用SynchronizationContext.SetSynchronizationContext(context);为当前线程设置同步上下文,需要在其他线程同步的时候 只需要context.Post方法或者context.Send方法即可同步。
此外,在CS程序中,winform,wpf都由针对SynchronizationContext类重写以便实现框架层面的需要,因为在cs程序中,所有控件的创建修改删除,等操作,都应该是由UI线程去完成,如果跨线程则会报错,同时在cs程序中使用了async和await,在await之后的环境上下文和同步上下文都是await之前的数据,所以在cs中await之后操作UI是不会有任何问题的,如果是需要在子线程中操作UI控件,则需要获取SynchronizationContext.Current对象获取当前同步上下文,或者使用winform重写之后的类WinformSynchronizationContext.Current获取同步上下文对象,然后去进行Post或者Send操作UI控件就不会报错。
今天在微信群讨论的时候,群友们在讨论跨线程操作的问题,便说到了这块,另外有个老哥说到,在子线程创建控件对象添加到窗体中,然后在操作的时候会报错,针对这个,我测试了之后,在子线程中创建TextBox,主线程给Text赋值,不会报错导致一场,然后我就猜测控件都是继承于Control类,那应该是Control类和SynchronizationContext类做了关联,导致虽然是子线程创建的对象,但是同样是属于主线程的,随后我去翻看了源码,验证了我的猜想。在下面的图中,如果我们在子线程new TextBox(),是走到了Contrl()这个构造方法,然后走到了internal Control的构造方法,参数autoInstallSyncContext是true,

然后调用了WindowsFormSynchronizationContext.InstallIfNeeded()方法,在这个方法我们最终看到子线程创建的控件最终还是属于UI线程的同步上下文的,为此我用代码做了验证。


在代码中执行这段代码,在Task.Run里面加入断点,就可以看到,在new TextBox之前,SynchronizationContext.Current获取到的是null,在之后获取到的是WindowsFormsSynchronizationContext的对象,由此可以看出所有的Control控件,哪怕都在子线程中创建,其也依旧属于UI线程。
await AddText();this.Controls.Add(TextBox) ;
JextBox.Text = "111”;
public Task AddText()
{
var con=WindowsFormsSynchronizationContext.Current;
return Task.Run(() =>
{
var c = SynchronizationContext.Current;
TextBox = new TextBox(); var b = SynchronizationContext.Current;
});
}
结语
今天的分享就到此结束了,对于async和await,更深层次的其实还是上下文流转,用不用新线程,是有TaskScheduler决定,线程复用是有ThreadPool决定,并且,异步不一定开启新线程,那不然委托异步,控件异步 是不是都开了新线程,卖个关子,有待你们去进行验证,如有疑问,欢迎大家进群讨论6406277,或者822074314都行,群内有很多大佬可以一起学习进步,另外也可以看群里有没有叫四川观察的,基本上就是咯,咱们下次再见


【C#异步】异步多线程的本质,上下文流转和同步的更多相关文章
- C#中异步和多线程的区别
C#中异步和多线程的区别是什么呢?异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性.甚至有些时候我们就认为异步和多线程是等同的概念.但是,异步和多线程还是有一些区别的.而这些区 ...
- C#用委托实现异步,异步与多线程的异同
异步与多线程的区别(转) 一.异步和多线程有什么区别?其实,异步是目的,而多线程是实现这个目的的方法.异步是说,A发起一个操作后(一般都是比较耗时的操作,如果不耗时的操作就没有必要异步了),可以继续自 ...
- 【Java_基础】并发、并行、同步、异步、多线程的区别
1. 并发:位于同一个处理器上的多个已开启未完成的线程,在任意一时刻系统调度只能让一个线程获得CPU资源运行,虽然这种调度机制有多种形式(大多数是以时间片轮巡为主).但无论如何,都是通过不断切换需要运 ...
- IOS异步和多线程操作&&在sqlite3中的应用
1,数据库I/O操作(异步) 数据库本身是存储在磁盘上.访问和修改数据库,即对磁盘进行读写,即I/O操作. 磁盘属于计算机硬件,具有DMA能力,不需要CPU干预,可以实现异步操作. I/O操作一般是消 ...
- PHP中实现异步调用多线程程序代码
本文章详细的介绍了关于PHP中实现异步调用多线程方法,下面我们以给1000个用户发送一封推荐邮件,用户输入或者导入邮件账号了提交服务器执行发送来讲述. 比如现在有一个场景,给1000个用户发送一封推荐 ...
- c#异步和多线程有什么区别和联系?
异步和多线程可以说没有必然的联系,只能说异步可以通过多线程实现而已要理解这些东西,你得具备很多相关的知识,操作系统原理,编译原理等简单地来说,计算机或者说CPU执行你的代码都是顺序执行的,当前的语句没 ...
- 编写高质量代码改善C#程序的157个建议——建议71:区分异步和多线程应用场景
建议71:区分异步和多线程应用场景 初学者有时候会将异步和多线程混为一谈.如果对它们之间的区别不是很清楚,很容易写出下面这样的代码: private void buttonGetPage_Click( ...
- C#中的异步和多线程
许多开发人员对异步代码和多线程以及它们的工作原理和使用方法都有错误的认识.在这里,你将了解这两个概念之间的区别,并使用c#实现它们. 我:"服务员,这是我第一次来这家餐厅.通常需要4个小时才 ...
- C#异步和多线程以及Thread、ThreadPool、Task区别和使用方法
本文的目的是为了让大家了解什么是异步?什么是多线程?如何实现多线程?对于当前C#当中三种实现多线程的方法如何实现和使用?什么情景下选用哪一技术更好? 第一部分主要介绍在C#中异步(async/awai ...
- (转)Linux下通过rsync与inotify(异步文件系统事件监控机制)实现文件实时同步
Linux下通过rsync与inotify(异步文件系统事件监控机制)实现文件实时同步原文:http://www.summerspacestation.com/linux%E4%B8%8B%E9%80 ...
随机推荐
- 使用JavaScript制作一个页面的电子时钟
题目:做一个电子时钟,显示当前的年月日,时分秒,要求自动变化. 案例分析: 1.使用一个div盒子来展示时钟的内容: 2.将盒子在JavaScrip里面获取div盒子: 3.我们需要一个定时器setI ...
- 配置php-fpm识别php文件访问
以前是装的集成环境,没有想到装完Nginx + PHP + MySQL 启动nginx 服务,出现页面: 如果访问120.25.216.6/index.php 就会变成下载 之所以会这样是因为2个原因 ...
- 图书管理系统、聚合函数、分组查询、F与Q查询
目录 图书管理系统 1.表设计 2.首页搭建.展示 书籍的添加 书籍编辑 书籍删除 聚合函数 Max Min Sum Count Avg 分组查询 按照表分组 按照字段分组 F与Q查询 F查询 Q查询 ...
- JavaScript:显式转换数据类型:如何转换为数值、字符串和布尔值类型?
JS的运算符以及某些内置函数,会自动进行数据类型的转换,方便计算,即隐式转换数据类型: 但是很多时候,我们希望可以手动控制数据类型的转换,即显示转换数据类型: 转换为字符串 String()函数 使用 ...
- Windows上将linux目录映射网络驱动器
我有两台PC,一台操作用的Windows,一台linux.为了方便对linux目录的文件操作.需要在Windows上将linux中的/fdsk目录映射为网络驱动器. a.首先要将linux安装成为sa ...
- openEuler 部署Kubernetes(K8s)集群
前言 由于工作原因需要使用 openEuler,openEuler官方文档部署K8s集群比较复杂,并且网上相关资料较少,本文是通过实践与测试整理的 openEuler 22.03 部署 Kuberne ...
- [Leetcode] 寻找数组的中心索引
题目 代码 class Solution { public: int pivotIndex(vector<int>& nums) { int right=0; for(auto i ...
- [Unity]Update()与FixedUpdate()
Update()介绍 首先我们从官方文档的介绍了解: MonoBehaviour.Update() Description Update is called every frame, if the M ...
- .Net开发的系统安装或更新时如何避免覆盖用户自定义的配置
我们开发的系统,有时候会包含一些配置信息,需要用户在系统安装后自己去设置,例如我们有一个GPExSettings.xml文件,内容如下. <GPExSettings ArcPythonPath= ...
- 浅谈RMQ问题
RMQ:question 有一个长度为 N N N的数组,数组中的数是无序的( 1 < = n < = 5 ∗ 1 0 5 1<=n<=5*10^5 1<=n<=5 ...