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

news/2025/1/21 5:57:31/文章来源:https://www.cnblogs.com/ggtc/p/18229724

在上一篇通信协议碰到了多线程,阻塞、非阻塞、锁、信号量...,会碰到很多问题。因此我感觉很有必要研究多线程异步编程

首先以一个例子开始

image

我说明一下这个例子。
这是一个演示异步编程的例子。

  • 输入job [name],在一个同步的Main方法中,以一发即忘的方式调用异步方法StartJob()
  • 输入time,调用同步方法PrintCurrentTime()输出时间。
  • 输出都带上线程ID,便于观察。
    可以看到,主线程不会阻塞。主线程在同步方法中使用一发即忘的方式调用异步方法时,在异步方法中碰到阻塞时,主线程返回同步方法中继续执行。而异步方法在另一个线程中继续执行。
    程序如下
internal class Program
{static void Main(string[] args){while (true){Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Enter 'time' to get current time or 'job [name]' to start a job:");string input = Console.ReadLine();if (input.StartsWith("time")){// 输出当前时间PrintCurrentTime();}else if (input.StartsWith("job")){// 启动一个异步任务,执行指定的工作string[] parts = input.Split(new char[] { ' ' }, 2);string jobName = parts.Length > 1 ? parts[1] : string.Empty;StartJob(jobName);}else{Console.WriteLine("Invalid input. Please try again.");}}}static void PrintCurrentTime(){Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Current time: {DateTime.Now}");}static async void StartJob(string jobName){// 获取主线程的线程 IDint mainThreadId = Thread.CurrentThread.ManagedThreadId;// 检查是否在主线程上bool onMainThread = Thread.CurrentThread.ManagedThreadId == mainThreadId;Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Starting job '{jobName}'. This will take 10 seconds...");// 输出主线程上下文移动情况Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Main thread context moved to new thread: {(!onMainThread)}");await Task.Delay(10000); // 模拟任务需要10秒钟完成// 输出任务完成信息及上下文移动情况Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Job '{jobName}' completed. Main thread context moved to new thread: {(!onMainThread)}");}}

上下文流转

一个方法从一个线程代码栈被切换,或者说被剪切到另一个线程代码栈上去,可以称为上下文流转
这对于理解异步编程是一个重要的点。
但由于上面的程序缺少必要变量,我需要在不同位置加几个变量,来展示上下文确实被移动了。

static async void StartJob(string jobName)
{int mainThreadId = Thread.CurrentThread.ManagedThreadId;// 检查是否在主线程上bool onMainThread = Thread.CurrentThread.ManagedThreadId == mainThreadId;...
}

image
可以看到onMainThread一直为False,这个变量从线程1移动到线程5
而且bool是值类型,在栈上面,这说明StartJob这段代码确实移动到线程5的栈上面去了。(每个线程都有一个调用栈)

使用VS调试窗口监视线程

想要再进一步,更清晰的话说明上下文流转的话,那就得监视这两个线程栈的内容了。万幸的是 vs提供了这个功能,调试 > 窗口 > 并行堆栈

  • 命中断点时,StartJob方法在主线程24876上
    image

  • 10秒后再次命中,StartJob方法跑到了任务线程上。而主线程现在在Main函数的Console.ReadLine()那里阻塞
    image

  • 代码阻塞与线程阻塞
    在上面的例子中我们引出两种现象,代码阻塞线程阻塞
    代码阻塞时,线程不一定阻塞,原线程没有阻塞,去执行别的代码了,而由新线程接手当前上下文和调用栈阻塞在这里,比如这里的await Task.Delay(10000)
    代码阻塞时线程也可能阻塞,比如lock(lockObj)Console.ReadLine()
    为了方便,我们姑且这样命名吧

    • 代码阻塞时,线程不阻塞称之为等待await
    • 代码阻塞时,线程也阻塞称之为阻塞block
  • 为什么有两个箭头
    这里为什么有线程24666和27548两个NET TP Worker(.NET Thread Pool (TP) Worker)?据chatGPT解释,Delay语句在线程池中找了一个线程去执行,一旦延迟时间到达,StartJob会在其中一个线程池线程上恢复执行。计时是一个线程,恢复上下文是另一个线程。Delay就代表了我们的那个耗时线程(不是异步方法所在线程)。
    既然有两个线程的联动,其中就出现了一些熟悉的东西。信号量Semaphore,一次性信号量的消耗TrySetResult,但详细过程我还不清楚。
    MSDN上的例子也是这样
    image

以同步的方式进行异步编程

原来把异步方法的上下文移动到新线程N,保证主线程不阻塞(脱离主线程U)。然后N用第三个线程C执行耗时任务,最后把C结果给位于N中的上下文。
站在代码编写者的角度,不特意去看线程的话,就不会注意到异步方法的上下文从一个线程跑到另一个线程上去了。这就是所谓的以同步的方式进行异步编程。
那么线程N的执行就明晰了。先保存上下文,然后启用新线程C进行耗时任务,并阻塞。等C使用信号量或其他什么通知N时,N再根据C的结果继续执行。
可以这样总结

  • asyncawait是一个语法糖。
  • 以同步的方式进行异步编程的方式是使用语法糖,以同步的方式书写代码,然后编译成适当的异步的实现。

我列出几种可能的异步的实现

1. 异步状态机

  • 异步状态机是C#编译async语法糖的实现方式
  • 异步方法StartJob将会被编译成一个同步方法StartJobAsync和一个状态机StartJobAsyncMachine
  • 状态机流转上下文的方式是将新线程用到的变量提升为字段,储存于可被线程共享的进程堆中
  • MoveNext方法可以被不同线程执行,这是关键
点击查看代码
internal class Program
{...internal static void StartJobAsync(string jobName){StartJobAsyncMachine stateMachine = new StartJobAsyncMachine();stateMachine.builder = AsyncVoidMethodBuilder.Create();stateMachine.jobName = jobName;stateMachine.state = -1;stateMachine.builder.Start(ref stateMachine);}public sealed class StartJobAsyncMachine : IAsyncStateMachine{public int state;public AsyncVoidMethodBuilder builder;private TaskAwaiter taskAwaiter;//形参会编译成public字段public string jobName;//被新线程使用的局部变量会编译成private字段private bool onMainThread;private void MoveNext(){int num = state;try{TaskAwaiter awaiter;if (num != 0){// 获取主线程的线程 IDint mainThreadId = Thread.CurrentThread.ManagedThreadId;// 检查是否在主线程上onMainThread = Thread.CurrentThread.ManagedThreadId == mainThreadId;Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Starting job '{jobName}'. This will take 10 seconds...");// 输出主线程上下文移动情况Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Main thread context moved to new thread: {(!onMainThread)}");awaiter = Task.Delay(10000).GetAwaiter();if (!awaiter.IsCompleted){num = (state = 0);taskAwaiter = awaiter;StartJobAsyncMachine stateMachine = this;builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);return;}}else{awaiter = taskAwaiter;taskAwaiter = default(TaskAwaiter);num = (state = -1);}awaiter.GetResult();// 输出任务完成信息及上下文移动情况Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Job '{jobName}' completed. Main thread context moved to new thread: {(!onMainThread)}");}catch (Exception exception){state = -2;builder.SetException(exception);return;}state = -2;builder.SetResult();}void IAsyncStateMachine.MoveNext(){this.MoveNext();}private void SetStateMachine(IAsyncStateMachine stateMachine){}void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine){this.SetStateMachine(stateMachine);}}
}

StartJobAsync的调用和原方法等效。我在Main中在加一种指令jobMachine调用StartJobAsync。原来的改为job空格

else if (input.StartsWith("jobMachine "))
{// 启动一个异步任务,执行指定的工作string[] parts = input.Split(new char[] { ' ' }, 2);string jobName = parts.Length > 1 ? parts[1] : string.Empty;StartJobAsync(jobName);
}

image

2. 协程

这种方法到底叫协程还是异步迭代器,我不太分得清,但目的是能够达到的,我暂且就叫做协程好了。
虽然这种做法就像脱裤子放屁,因为协程最后也会编译成状态机。这个例子主要是为了演示直观。
理论上,C#中的异步/等待(async/await)语法并不是直接编译成协程的,而是由编译器生成状态机(state machine)来管理异步操作。但是,我们可以通过理解协程的工作原理以及C#异步/等待模型的特性,来描绘一种可能的编译结果。
这里我写了一个基于协程的异步的实现。效果和原来的等同。

  • 原理
    和状态机实现基本一样。对于每个async方法生成一个协程。而且在异步方法嵌套时,那么async方法内部的async方法在编译时就不需要开一个新线程了。要不然得多少线程。
internal class Program
{static void Main(string[] args){while (true){...else if (input.StartsWith("jobCorotine ")){// 启动一个异步任务,执行指定的工作string[] parts = input.Split(new char[] { ' ' }, 2);string jobName = parts.Length > 1 ? parts[1] : string.Empty;StartJobAsync_2(jobName);}...}}#region 异步协程static void StartJobAsync_2(string jobName){StartJobAsyncCorotine startJobCorotine = new StartJobAsyncCorotine();startJobCorotine.jobName = jobName;var enumerator = startJobCorotine.DelayedOperations();var iterator = enumerator.GetEnumerator();bool next = false;while (true){next = iterator.MoveNext();if (!iterator.Current.IsCompleted){//异步方法中存在耗时任务,切换到新线程break;}next = false;}if (next == false){return;}//异步方法存在耗时任务,切换上下文到新线程Task.Run(() =>{do{if (!iterator.Current.IsCompleted){//创建耗时任务线程进行耗时任务Task.Run(() =>{iterator.Current.GetResult();}).Wait();}}while (iterator.MoveNext());});}public sealed class StartJobAsyncCorotine{//形参因为需要运行时赋值,只能写成字段的形式public string jobName;public int Count = 1;public IEnumerable<TaskAwaiter> DelayedOperations(){TaskAwaiter awaiter1;// 获取主线程的线程 IDint mainThreadId = Thread.CurrentThread.ManagedThreadId;// 检查是否在主线程上bool onMainThread = Thread.CurrentThread.ManagedThreadId == mainThreadId;Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Starting job '{jobName}'. This will take 10 seconds...");// 输出主线程上下文移动情况Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Main thread context moved to new thread: {(!onMainThread)}");awaiter1 = Task.Delay(10000).GetAwaiter(); // 模拟任务需要10秒钟完成//出去判断这是否是耗时任务以切换线程yield return awaiter1;// 输出任务完成信息及上下文移动情况Console.WriteLine($"(Thread ID: {Thread.CurrentThread.ManagedThreadId}) Job '{jobName}' completed. Main thread context moved to new thread: {(!onMainThread)}");}}#endregion
}
  • 效果确实和原来一样

image

3. 闭包

这真不需要多说,通过闭包进行捕获上下文真的是太常见了,Ajax中用到吐🤮

带返回值的上下文流转

StartJob是没有返回值的,假如我们需要一个返回值呢,比如一个bool,用于判断接下来的执行流程。
调用异步方法StartJob的同步方法Main之间存在着绝对的分界线——两个线程。同步方法不会被交给异步方法中的那个新线程,没法在同步方法中以同步的方式进行异步编程
唯一的一点看头是,至少Task还给我们留下了一个回调ContinueWith可用。但条件允许的话,何不把回调的内容写在异步方法内部呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/721815.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【日记】遇到了一个 “不愿睁眼看世界也没受过社会毒打” 的逆天群友(464 字)

正文今天坐在柜台玩了一天手机…… 手机都玩没电了快。下午在劝一个群友睁眼看世界,实在劝不动。他真的太逆天了,我不清楚这么高学历的人,怎么能说出这么天真的话。逆天又离谱。晚上的时间几乎全在做家务。平时晚上都是跳舞来着,没时间也不想动。意外地花了挺长时间。扫了地…

友链

Blogroll 友链愿我如星君如月,夜夜流光相皎洁。

Linux 中date命令

date主要用于显示日期,若是不以加号作为开头,则表示要设定时间,而时间格式为 MMDDhhmm[[CC]YY][.ss],其中 MM 为月份,DD 为日,hh 为小时,mm 为分钟,CC 为年份前两位数字,YY 为年份后两位数字,ss 为秒数。 001、最基本的用法[root@PC1 test2]# date ## 输出日…

Linux 中date

date主要用于显示日期,若是不以加号作为开头,则表示要设定时间,而时间格式为 MMDDhhmm[[CC]YY][.ss],其中 MM 为月份,DD 为日,hh 为小时,mm 为分钟,CC 为年份前两位数字,YY 为年份后两位数字,ss 为秒数。 001、最基本的用法[root@PC1 test2]# date ## 输出日…

类加载器

双亲委派机制类加载器中的核心方法 loadClass protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?&g…

IceRPC之依赖注入快乐的RPC

作者引言很高兴啊,我们来到了IceRPC之依赖注入>快乐的RPC,基础引导,打好基础,才能让自已不在迷茫,快乐的畅游世界。依赖注入和IceRPC了解 IceRPC (C#) 如何为依赖注入(DI)提供支持。DI作为可选功能 DI的第一条规则是:不要引入对DI的依赖。 IceRPC (C#) 将此规则放首要…

图床

表视图触发器本文来自博客园,作者:zhywyt,转载请注明原文链接:https://www.cnblogs.com/zhywyt/p/18238527

阅读习惯

本学期开始时我的阅读量大约在50小时左右,通过这一学期读电子书,我的阅读时长为200小时,大约增加了150小时,在这150小时中,我每天会抽出半个小时来读书,这其中包括课外书和娄老师推荐的书目,比方说《学会提问》、《批判性思维》,目前在书架上的书有四本,在每天坚持读书…

纯CSS+单个div实现抖音LOGO

纯CSS+单个div就能绘制抖音LOGO。 主要借助了两个伪元素实现了整体结构,借助了 drop-shadow 生成一层整体阴影。 drop-shadow 只能是单层阴影,所以另一层阴影需要多尝试。 contrast(150%) brightness(110%) 则可以增强图像的对比度和亮度,更贴近抖音LOGO的效果。纯CSS+单个d…

完美解决SqlServer2012启动报错(cannot find one or more components.Please reinstall the application。)

出现这问题的原因是,vs组件没有安装或被卸载,重新安装这个组件即可。微软官网下载忒麻烦,这里分享一下 我用夸克网盘分享了「vs shell2010安装文件.rar」链接:https://pan.quark.cn/s/0347cf062d65

web开发之浏览器扩展插件开发-chrome浏览器扩展插件开发-入门

一.起步:开始,开发一个浏览器扩展程序: 参考文档: https://developer.chrome.google.cn/docs/extensions/get-started/tutorial/hello-world?authuser=19&%3Bhl=zh-cn&hl=zh-cn1.配置文档:manifest.json 对应文件 :新建manifest.json /popup.html / icon-green.…

Docker的资源限制

目录一、什么是资源限制1、Docker的资源限制2、内核支持Linux功能3、OOM异常4、调整/设置进程OOM评分和优先级4.1、/proc/PID/oom_score_adj4.2、/proc/PID/oom_adj4.3、/proc/PID/oom_score二、容器的内存限制1、实现原理2、命令格式及指令参数2.1、命令格式2.2、指令参数3、案…