C#基础系列-关于异步编程状态机
一、背景
在上一篇的异步编程初识中,已经详细介绍了C#异步编程的演化,到当前使用Task+async/await模式实现异步编程。本篇继续基于异步编程内容深入理解该模式的原理,要理解该模型必须回答下列几个问题,使用async/await关键字相比Task的改进点是什么?编译器是如何将其转换成状态机的?如何使用async/await?
二、内容
1、async/await与Task的关系
在TPL模式中,使用Task核心抽象,极大改进了异步编程的方式,在多个异步编程的维度得到支持。基于Task的方式,引入async/await关键字,因为使用Task在上一章节的案例中要创建Task任务、使用延续+回调完成异步编程,整体使用方式与同步编程还是有差异,所以在Task的基础进一步演化,实现了以同步写法表达异步控制流。
总结一句话:Task负责承载异步操作的状态、结果和延续;async/await负责把Task的异步执行流程转换成可读的顺序代码。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using static System.Runtime.InteropServices.JavaScript.JSType; namespace tqf.study.demo { public class AsyncDemo { public static async Task Main(string[] args) { Console.WriteLine($"【线程 ID: {Thread.CurrentThread.ManagedThreadId}】异步操作开始: {DateTime.Now:HH:mm:ss.fff}"); // await GetDataAsyncAwait(); GetDataTask(); Console.WriteLine($"【线程 ID: {Thread.CurrentThread.ManagedThreadId}】异步操作结束: {DateTime.Now:HH:mm:ss.fff}"); Console.ReadLine(); } /// <summary> /// 使用Task异步操作获取结果 /// </summary> static void GetDataTask() { // 执行下面一系列操作 // 获取异步任务返回的结果 Task<string> task = GetDataAsync(); // 延续注册 task.ContinueWith( t => { // 回调嵌套 // 获取结果 if (t.IsCompleted) { string result = t.Result; Console.WriteLine($"【线程 ID: {Thread.CurrentThread.ManagedThreadId}】异步操作结果:{result} {DateTime.Now:HH:mm:ss.fff}"); } // 判断异常 if (t.IsFaulted) { Console.WriteLine(t.Exception.ToString()); return; } // 判断取消 if (t.IsCanceled) { Console.WriteLine("异步任务已经取消"); return; } }); } /// <summary> /// 使用async/await的方式,同步代码顺序执行完成异步任务 /// </summary> /// <returns></returns> static async Task GetDataAsyncAwait() { try { string result = await GetDataAsync(); Console.WriteLine($"【线程 ID: {Thread.CurrentThread.ManagedThreadId}】异步操作结果:{result} {DateTime.Now:HH:mm:ss.fff}"); } catch (Exception ex) { Console.WriteLine(ex); } } /// <summary> /// 异步任务(包装一个异步任务结果返回,使用sleep模拟异步任务耗时) /// </summary> /// <returns></returns>
static async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "test";
}
}
}
【线程 ID: 1】异步操作开始: 23:33:37.215 【线程 ID: 1】异步操作结束: 23:33:42.251 【线程 ID: 4】异步操作结果:测试 23:33:42.253
通过上面代码对比,可以理解为Task是载体,标识异步任务操作的进行中、已完成、已取消、失败。await是挂载点,根据任务是否完成把async的方法挂起,线程释放,继续执行其他操作,Task完成,则继续恢复执行。
注意async不会自动开启线程,通过Task,await不是阻塞线程进行等待,是挂起方法,释放线程,其底层操作依据是通过Task + continuation + 状态机。比较精准的总结就是Task 是异步的“数据结构和执行载体”,async/await 是异步的“控制流语法糖”。
2、async/await的本质内容
在上面介绍到其本质是Task + continuation + 状态机,async/await标识在编译器编译阶段构建一个状态机。既然async/await的是通过编译器编译成状态机则通过编译结果代码进行分析是如何完成整个操作的。首先是定义一个异步方法,使用async/await进行标识执行,然后查看编译阶段生成的状态机代码。
public async Task<string> DownloadDataAsync(string url) { using var client = new HttpClient(); string data = await client.GetStringAsync(url); return data.Length.ToString(); }
[CompilerGenerated] private struct <DownloadDataAsync>d__1 : IAsyncStateMachine { public int <>1__state; // 状态编号 public AsyncTaskMethodBuilder<string> <>t__builder; // 任务构建器 public string url; // 提升的参数 private HttpClient client; // 提升的局部变量 private TaskAwaiter<string> <>u__1; // 当前等待的 awaiter private string <>s__2; // 提升的局部变量 data public void MoveNext() { int num = <>1__state; string result; try { TaskAwaiter<string> awaiter; if (num != 0) { // === 第一个代码段:await 之前 === client = new HttpClient(); awaiter = client.GetStringAsync(url).GetAwaiter(); if (!awaiter.IsCompleted) { // 任务未完成 → 挂起 num = (<>1__state = 0); <>u__1 = awaiter;
// 没完成?注册回调,然后直接 return(方法"挂起"了) <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); return; // 方法在此返回,线程被释放 } } else { // === 恢复执行 === awaiter = <>u__1; <>u__1 = default(TaskAwaiter<string>); num = (<>1__state = -1); } // === 第二个代码段:await 之后 === string data = awaiter.GetResult(); <>s__2 = data; result = data.Length.ToString(); } catch (Exception exception) { <>1__state = -2; <>t__builder.SetException(exception); // 如果把异常返回给调用方,塞入Task<string> return; } <>1__state = -2; client?.Dispose(); <>t__builder.SetResult(result); // 把结果交还给调用方法,塞入Task<string> } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { <>t__builder.SetStateMachine(stateMachine); } }
主要操作a.局部变量提升为字段,因为 await 挂起时,方法栈帧会被销毁。如果局部变量保存在栈上,恢复时数据就会丢失。编译器将所有跨 await 的变量提升为状态机字段,这是闭包之外另一种变量捕获形式。b.通过await对代码分段为await前与后,每次resume都是从movenext方法重新进入,根据state状态决定接上那一段执行。await的方法完成一次注册回调+可能挂起。
3、async/await的执行流程图
上面使用代码的方式解释了状态的实现过程,关键内容,下面是整个执行的时序图,通过时序图总结性的描述async/await实现异步编程的。


async 方法不是一口气跑完,而是跑一段就“存档”(记下 state),等 Task 完成后再“读档”接着跑。全程没有阻塞,只是把一段代码拆成了好几段执行。对async/await的总结性一个state字段的Movenext方法,把原来方法按照await切断,使用判断执行。
4、使用async/await的案例
async/await的使用比较频繁,比如各框架的IO,文件流操作、网络流操作、数据库的数据流操作都会提供Async结尾的方法,都是异步执行,在实际开发中会使用Task构建异步任务,进行异步操作。
三、总结
在C#中包含很多语法糖的写法,比如属性相关的get/set、对象/集合初始化、类型推断与匿名类型、委托与Lambda、字符串关键字$/""""""/@、LINQ、using、模式匹配、yield、lock,record等等。使用语法糖极大的简化代码的编写,代码更简洁,可读性高,但编译后会被转换成更基础 CLR 指令的语法特性,可以通过反编译还原IL进行查看。但是语法糖很多时候隐藏技术细节,所以不容易理解。针对async/await的代码糖的问题详细说明了代码糖。
所以使用Task+async/await的方式极大简化异步编程,趋向于同步编程的方式使用。

浙公网安备 33010602011771号