async/await 贴脸输出,这次你总该明白了
出来混总是要还的
最近在准备记录一个.NET Go核心能力的深度对比, 关于.NET/Go的异步实现总感觉没敲到点上。
async/await是.NET界老生常谈的话题,每至于此,状态机又是必聊的话题,但是状态机又是比较晦涩难懂的话题。
[一线码农大佬]在博客园2020年写的《await,async 我要把它翻个底朝天,这回你总该明白了吧》手把手实现了异步状态机,这篇文章很是经典, 但是评论区很多人还是在吐槽看不懂, 我也看的不是很懂。
以我浅薄的推测:
- 一线大佬的知识体系太宽太深,有的验证点在文字之外,需要我们自己去确认。
- 有些内容太细节,挖的太深,出不来。
- 很多人不熟悉状态机设计模式, 导致看大佬文章,知其然不知其所以然。
我以前用Go语言演示了状态机: 我是状态机,有一颗永远骚动的机器引擎, 当时有粉丝留言让用.NET 实现状态机, 这篇文章也算是对粉丝的喊话。
状态机:一颗永远骚动的机器引擎
状态机是一种行为设计模式,它允许对象在其内部状态改变时改变其行为。看起来好像对象改变了它的类。
请仔细理解上面每一个字。
我们以自动售货机为例,为简化演示,我们假设自动售货机只有1种商品, 故自动售货机有itemCount
、itemPrice
2个属性
不考虑动作的前后相关性,自动售货机对外暴露4种行为:
- 给自动售货机加货
addItem
- 选择商品
requestItem
- 付钱
insertMoney
- 出货
dispenseItem
重点来了,当发生某种行为,自动售货机会进入如下4种状态之一, 并据此状态做出特定动作, 之后进入另外一种状态.....
- 有商品
hasItem
- 无商品
noItem
- 已经选好商品
itemRequested
- 已付钱
hasMoney
当对象可能处于多种不同的状态之一、根据传入的动作更改当前的状态, 继续接受后续动作,状态再次发生变化.....
这样的模式类比于机器引擎,周而复始的工作和状态转化,这也是状态机的定语叫“机Machine”的原因。
有了以上思路,我们尝试沟通UML 伪代码
状态机设计模式的伪代码实现:
- 所谓的机器Machine维护了状态切换的上下文
- 机器对外暴露的行为,驱动机器的状态变更
- 机器到达特定的状态 只具备特定的行为,其他行为是不被允许的
Go版本的售货机(状态机设计模式)的源码,请参见原文https://www.cnblogs.com/JulianHuang/p/15304184.html。
async/await贴脸开大
还是以一线码农大佬的异步下载为例:
编译器词法分析定位到async/await语法糖,就会为开发者生成状态机模板代码, 核心是MoveNext函数,里面包含了根据状态机status而执行不同代码的模板代码。
一个新出炉的状态机包含如下属性 :
(1) 初始化的状态机,以async所在的函数名命名,示例状态机为<GetResult>d__1
;
(2)车钥匙启动状态机之后,立马返回,这正是异步编程
的内涵。
一个简单的、成功的状态机转化如图:
1. 状态机初始化点
- state= -1;
<!----> Program.<GetResult>d__1 stateMachine = new Program.<GetResult>d__1(); stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create(); // return new AsyncTaskMethodBuilder() stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start<Program.<GetResult>d__1>(ref stateMachine); return stateMachine.<>t__builder.Task;
internal void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { // ReSharper disable RedundantCast if ((object)stateMachine == null) // ReSharper restore RedundantCast throw new ArgumentNullException("stateMachine"); stateMachine.MoveNext(); }
只有一个行为: 进入MoveNext
方法。
Task 是一个表示异步操作的对象。它可以用于等待异步操作的完成,并获取操作的结果(如果有)。
TaskAwaiter 是一个结构体,表示等待 Task 的完成。通常不直接使用,而是由编译器在 async/await 语法糖中隐式地使用。
2. 启动异步任务点(第一次进入MoveNext方法)
stateMachine进入启动异步任务
状态,迅速设置等待Task完成的的TaskAwaiter
对象,
并调用[AwaitUnsafeOnCompleted()
向awaiter对象注册回调函数MoveNext] (https://github.com/OmerMor/AsyncBridge/blob/aa806b61be32363934378ffc2df979dc8b34ea88/src/AsyncBridge/Runtime.CompilerServices/AsyncTaskMethodBuilder.cs#L328)
这是一个高层的注册函数
- 参数1:异步结果TaskAwaiter<TResult>
- 参数2: 当前状态机
<!----> int num1 = this.<>1__state; if (num1 != 0) { this.<client>5__1 = new WebClient(); awaiter = this.<client>5__1.DownloadStringTaskAsync(new Uri("http://cnblogs.com")).GetAwaiter(); if (!awaiter.IsCompleted) { this.<>1__state = num2 = 0; this.<>u__1 = awaiter; Program.<GetResult>d__1 stateMachine = this; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<GetResult>d__1>(ref awaiter, ref stateMachine); return; } }
状态2也是迅速返回,但是核心的注册函数已经注入。
会在IO数据就绪,事件通知到awaiter对象, 进而执行到awaiter上挂载的回调方法CompletionAction
, 这个在下文会详细分析。
3. 异步任务已完成点
- state = -1;
- taskAwaiter获取异步任务结果;
- 执行后继代码;
<!----> else { awaiter = this.<>u__1; this.<>u__1 = new TaskAwaiter<string>(); this.<>1__state = num2 = -1; } this.<>s__3 = awaiter.GetResult(); this.<content>5__2 = this.<>s__3; this.<>s__3 = (string) null; content52 = this.<content>5__2; // 后继代码段
4. 状态机终止点
- state =-2;
- 设置状态机最终返回值;
<!----> this.<>1__state = -2; this.<client>5__1 = (WebClient) null; this.<content>5__2 = (string) null; this.<>t__builder.SetResult(content52);
IL spy 反编译结果: 状态机轮转模板代码
以上四个状态的贴脸源码均截取自ILspy反编译结果,读者可将代码和状态轮转图对比。
一线码农大佬讲: 一个简单成功的async/await状态机会经历 2次MoveNext
动作 ,我是认同的。
一次是状态机启动,主动切换状态;
第二次是IO数据就绪,回调函数会执行原状态机的MoveNext
方法(主体是后继代码), 这个是在注册回调的时候确立的。
5. 第二次MoveNext
方法的注册现场、触发时机、执行原理
5.1 溯源第二次MoveNext
方法的注册现场
注册挂载MoveNext方法的核心对象是TaskAwaiter
, DownloadStringTaskAsync返回task对象之后,迅速转为taskAwaiter对象,
用户态对taskAwaiter对象挂载的completionAction
,实际就是状态机对象的MoveNext方法 。
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { try { var completionAction = coreState.GetCompletionAction(ref this, ref stateMachine); awaiter.UnsafeOnCompleted(completionAction); // 对taskAwaiter 对象挂载上回调函数completionAction } catch (Exception ex) { AsyncMethodBuilderCore.ThrowAsync(ex, null); } }
下面是awaiter挂载函数completionAction == 状态机的MoveNext方法 的注入堆栈
taskAwaiter实现
ICriticalNotifyCompletion
的UnsafeOnCompleted方法, 给了taskAwaiter添加后继任务的机会。
5.2 第二次MoveNext方法被调度的原理
接5.1 注册时调用的TaskAwaiter的静态函数,这里面有两个参数
public void UnsafeOnCompleted(Action continuation) { TaskAwaiter.OnCompletedInternal((Task) this.m_task, continuation, true, false); }
1> continueOnCapturedContext: 决定了异步操作完成后,是否应该在捕获的上下文中继续执行后续代码,默认是 true
这个参数也有很久的历史背景: 在.NetFramework带UI的程序中,需要在捕获的原UI线程执行后继代码,产生了同步上下文的概念; 现在在较新的.NET, 基于跨平台的愿景,其实已经放弃了带UI的程序,现在已经没有同步上下文的概念,兼容代码捕获的值为null, 后继代码在线程池线程中执行。
2> flowExecutionContext: 执行上下文是一个用于捕获和传播线程执行环境的机制,它包括安全上下文、同步上下文、文化信息等。这在多线程和异步编程中非常重要,因为它确保了上下文的一致性和安全性。
从上面的分析: 第二次MoveNext方法会被作为当前异步任务的后继任务,由线程池任务调度器来调度。
5.3 探究第二次MoveNext方法被触发的流程
从开启异步任务,到任务就绪,反馈到上层的注册函数, 这个很细致,涉及内核态和用户态的转换。
需要回看DownloadStringTaskAsync
是怎么实现的:
1> 在发起异步IO操作webClient.DownloadStringTaskAsync()的时候,.NET框架底层会使用传统的Begin/End异步编程模型,我们看到的上层是Task对象, 会有一个关键的TaskCompletionSource
对象负责传统异步编程模型到基于任务的编程模型的转换。
注册到底层的是由.NET框架和系统决定的, window 平台使用iocp,unix平台可能使用 epoll。
epoll 并不是回调机制,相反它是事件通知的机制,通过 epoll_create, epoll_ctl, epoll_wait 三个函数拿到关注的fd上发生的事件。
public Task DownloadFileTaskAsync(Uri address, string fileName) { // Create the task to be returned var tcs = new TaskCompletionSource<object?>(address); // Setup the callback event handler AsyncCompletedEventHandler? handler = null; handler = (sender, e) => HandleCompletion(tcs, e, (args) => null, handler, (webClient, completion) => webClient.DownloadFileCompleted -= completion); DownloadFileCompleted += handler; // Start the async operation. //底层是Begin/End异步模型 try { DownloadFileAsync(address, fileName, tcs); } catch { DownloadFileCompleted -= handler; throw; } // Return the task that represents the async operation return tcs.Task; }
2> IO线程接收到io操作完成的事件通知,会去更新.NET事件源TaskCompletionSource,更新底层的Task的状态到RunToCompletion。
private void HandleCompletion<TAsyncCompletedEventArgs, TCompletionDelegate, T>(TaskCompletionSource<T> tcs, TAsyncCompletedEventArgs e, Func<TAsyncCompletedEventArgs, T> getResult, TCompletionDelegate handler, Action<WebClient, TCompletionDelegate> unregisterHandler) where TAsyncCompletedEventArgs : AsyncCompletedEventArgs { if (e.UserState == tcs) { try { unregisterHandler(this, handler); } finally { if (e.Error != null) tcs.TrySetException(e.Error); else if (e.Cancelled) tcs.TrySetCanceled(); else tcs.TrySetResult(getResult(e)); // Attempts to transition the underlying Task<TResult> into the RanToCompletion state } } } public bool TrySetResult(TResult result) { bool rval = _task.TrySetResult(result); if (!rval) { _task.SpinUntilCompleted(); } return rval; }
3> 线程池线程会监控Task的生命周期,当任务执行完成,会标记完成,并从任务队列去定位后继任务去执行, 这块属于默认的线程池任务调度器的内容(也很庞大)读者自行翻阅。
6.结束语
本文重点从状态机设计模式的角度,演示了async/await语法糖的内部实现。
通过一个骚动的机器引擎,演示了开启异步任务---> 异步任务完成---> 设置状态机输出结果的全过程,而这4个状态的变迁又催生了.NET异步编程的带来的性能优势。
最后:本文是一线码农大佬《异步async/await底朝天》的狗尾续貂,respect !!!
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/18137189
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库