【C#异步】异步多线程的本质,上下文流转和同步

异步,本质,多线程,上下文 · 浏览次数 : 4052

小编点评

**Context** **SynchronizationContext** * 用于传播同步上下文的基本功能 * 可以称它为线程同步上下文 * 可以设置自己的同步上下文信息 * 在CS程序中,winform,wpf都由针对SynchronizationContext类重写以便实现框架层面的需要 **Thread** * 线程环境上下文 * 可以设置自己的同步上下文信息 * 在子线程中创建控件对象添加到窗体中,然后在操作的时候会报错 **Task** * 有TaskScheduler决定,线程复用是有ThreadPool决定 * 异步不一定开启新线程,那不然委托异步,控件异步 是不是都开了新线程 **ContextFlow** * 当子线程创建的控件对象最终还是属于UI线程的同步上下文的,为此我用代码做了验证

正文

引言

    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#异步】异步多线程的本质,上下文流转和同步

引言 net同僚对于async和await的话题真的是经久不衰,这段时间又看到了关于这方面的讨论,最终也没有得出什么结论,其实要弄懂这个东西,并没有那么复杂,简单的从本质上来讲,就是一句话,async 和await异步的本质就是状态机+线程环境上下文的流转,由状态机向前推进执行,上下文进行环境切换,

多线程合集(三)---异步的那些事之自定义AsyncTaskMethodBuilder

引言 之前在上一篇文章中多线程合集(二) 异步的那些事,async和await原理抛析,我们从源码去分析了async和await如何运行,以及将编译后的IL代码写成了c#代码,以及实现自定义的Awaiter,自定义异步状态机同时将本系列的第一篇文章的自定义TaskScheduler和自定义的Awai

C#异步编程是怎么回事(番外)

在上一篇通信协议碰到了多线程,阻塞、非阻塞、锁、信号量...,会碰到很多问题。因此我感觉很有必要研究多线程与异步编程。 首先以一个例子开始 我说明一下这个例子。 这是一个演示异步编程的例子。 输入job [name],在一个同步的Main方法中,以一发即忘的方式调用异步方法StartJob()。 输

C#异步有多少种实现方式?

前言 微信群里的一个提问引发的这个问题,有同学问:C#异步有多少种实现方式?想要知道C#异步有多少种实现方式,首先我们要知道.NET提供的执行异步操作的三种模式,然后再去了解C#异步实现的方式。 .NET异步编程模式 .NET 提供了执行异步操作的三种模式: 基于任务的异步模式 (TAP) ,该模式

C#委托

目录C# 委托委托是什么?基本语法委托的常见用法总结引用 C# 委托 委托是什么? ** 委托定义一种类型,该类型封装一个或多个方法(一个或多个方法指向委托实例)。** 委托是一种指向方法的引用。它允许您将方法存储在变量中,并像调用普通方法一样调用它们。委托通常用于事件处理 和异步编程。 基本语法

C# - 能否让 SortedSet.RemoveWhere 内传入的委托异步执行

若想充分利用 `RemoveWhere` 带来的性能优势,建议传入判断是否删除元素的委托内采取同步操作。若一定要在该委托内使用异步操作,可以采用本文中绕行的方法,但摈弃了 `RemoveWhere` 所带来的性能优势。

记一次RocketMQ消费非顺序消息引起的线上事故

应用场景 C端用户提交工单、工单创建完成之后、会发布一条工单创建完成的消息事件(异步消息)、MQ消费者收到消息之后、会通知各处理器处理该消息、各处理器处理完后都会发布一条将该工单写入搜索引擎的消息、最终该工单出现在搜索引擎、被工单处理人检索和处理。 事故异常体现 1、异常体现 从工单的流转记录发现、

在C#中使用RabbitMQ做个简单的发送邮件小项目

在C#中使用RabbitMQ做个简单的发送邮件小项目 前言 好久没有做项目了,这次做一个发送邮件的小项目。发邮件是一个比较耗时的操作,之前在我的个人博客里面回复评论和友链申请是会通过发送邮件来通知对方的,不过当时只是简单的进行了异步操作。 那么这次来使用RabbitMQ去统一发送邮件,我的想法是通过

[转帖][大数据]ETL之增量数据抽取(CDC)

https://www.cnblogs.com/johnnyzen/p/12781942.html 目录 1 CDC 概念 1.1 定义 1.2 需求背景 1.3 考察指标 2 CDC 常见解决方案 2.1 基于时间戳的CDC 【侵入式CDC + 异步CDC】 2.2 基于触发器的CDC 【侵入式C

C#学习笔记---异常捕获和变量

异常捕获 使用异常捕获可以捕获出现异常的代码块,防止因为异常抛出造成的程序卡死的情况发生。 try{}catch{}finally{}结构 //异常捕获 try { string str=Console.ReadLine(); int i=int.Parse(str); Console.WriteL