聊一聊 C#异步 任务延续的三种底层玩法
一:背景
1. 讲故事
最近聊了不少和异步相关的话题,有点疲倦了,今天再写最后一篇作为近期这类话题的一个封笔吧,下篇继续写我熟悉的 生产故障 系列,突然亲切感油然而生,哈哈,免费给别人看程序故障,是一种积阴德阳善的事情,欲知前世因,今生受者是。欲知来世果,今生做者是。
在任务延续方面,我个人的总结就是三类,分别为:
- StateMachine
- ContinueWith
- Awaiter
话不多说,我们逐个研究下底层是咋玩的?
二:异步任务延续的玩法
1. StateMachine
说到状态机大家再熟悉不过了,也是 async,await 的底层化身,很多人看到 async await 就想到了IO场景,其实IO场景和状态机是两个独立的东西,状态机是一种设计模式,把这个模式套在IO场景会让代码更加丝滑,仅此而已。为了方便讲述,我们写一个 StateMachine 与 IO场景 无关的一段测试代码。
internal class Program
{
static void Main(string[] args)
{
UseAwaitAsync();
Console.ReadLine();
}
static async Task<string> UseAwaitAsync()
{
var html = await Task.Run(() =>
{
Thread.Sleep(1000);
var response = "<html><h1>博客园</h1></html>";
return response;
});
Console.WriteLine($"GetStringAsync 的结果:{html}");
return html;
}
}
那这段代码在底层是如何运作的呢?刚才也说到了asyncawait只是迷惑你的一种幻象,我们必须手握辟邪宝剑斩开幻象显真身,这里借助 ilspy 截图如下:

从卦中看,本质上就是借助AsyncTaskMethodBuilder<string> 建造者将 awaiter 和 stateMachine 做了一个绑定,感兴趣的朋友可以追一下 AwaitUnsafeOnCompleted() 方法,最后状态机 <UseAwaitAsync>d__1 实例会放入到 Task.Run 的 m_continuationObject 字段。如果有朋友对流程比较蒙的话,我画了一张简图。

图和代码都有了,接下来就是眼见为实。分别在 AddTaskContinuation 和 RunContinuations 方法中做好埋点,前者可以看到 延续任务 是怎么加进去的,后者可以看到 延续任务 是怎么取出来的。


心细的朋友会发现这卦上有一个很特别的地方,就是 allowInlining=true,也就是回调函数(StateMachine)是在当前线程上一撸到底的。
有些朋友可能要问,能不能让延续任务 跑在单独线程上? 可以是可以,但你得把 Task.Run 改成 Task.Factory.StartNew ,这样就可以设置TaskCreationOptions参数,参考代码如下:
var html = await Task.Factory.StartNew(() =>{}, TaskCreationOptions.RunContinuationsAsynchronously);
2. ContinueWith
那些同处于被裁的35岁大龄程序员应该知道Task是 framework 4.0 时代出来的,而async,await是4.5出来的,所以在这个过渡期中有大量的项目会使用ContinueWith 导致回调地狱。。。 这里我们对比一下两者有何不同,先写一段参考代码。
internal class Program
{
static void Main(string[] args)
{
UseContinueWith();
Console.ReadLine();
}
static Task<string> UseContinueWith()
{
var query = Task.Run(() =>
{
Thread.Sleep(1000);
var response = "<html><h1>博客园</h1></html>";
return response;
}).ContinueWith(t =>
{
var html = t.Result;
Console.WriteLine($"GetStringAsync 的结果:{html}");
return html;
});
return query;
}
}
从卦代码看确实没有asyncawait简洁,那 ContinueWith 内部做了什么呢?感兴趣的朋友可以跟踪一下,本质上和 StateMachine 的玩法是一样的,都是借助 m_continuationObject 来实现延续,画个简图如下:

代码和模型图都有了,接下来就是用 dnspy 开干了。。。还是在 AddTaskContinuation 和 RunContinuations 上埋伏断点观察。


从卦中可以看到,延续任务使用新线程来执行的,并没有一撸到底,这明显与 asyncawait 的方式不同,有些朋友可能又要说了,那如何实现和StateMachine一样的呢?这就需要在 ContinueWith 中新增 ExecuteSynchronously 同步参数,参考如下:
var query = Task.Run(() => { }).ContinueWith(t =>
{
}, TaskContinuationOptions.ExecuteSynchronously);

3. Awaiter
使用Awaiter做任务延续的朋友可能相对少一点,它更多的是和 StateMachine 打配合,当然单独使用也可以,但没有前两者灵活,它更适合那些不带返回值的任务延续,本质上也是借助 m_continuationObject 字段实现的一套底层玩法,话不多说,上一段代码:
static Task<string> UseAwaiter()
{
var awaiter = Task.Run(() =>
{
Thread.Sleep(1000);
var response = "<html><h1>博客园</h1></html>";
return response;
}).GetAwaiter();
awaiter.OnCompleted(() =>
{
var html = awaiter.GetResult();
Console.WriteLine($"UseAwaiter 的结果:{html}");
});
return Task.FromResult(string.Empty);
}
前面两种我配了图,这里没有理由不配了,哈哈,模型图如下:

接下来把程序运行起来,观察截图:


从卦中观察,它和StateMachine一样,默认都是 一撸到底 的方式。
三:RunContinuations 观察
这一小节我们单独说一下 RunContinuations 方法,因为这里的实现太精妙了,不幸的是Dnspy和ILSpy反编译出来的代码太狗血,原汁原味的简化后代码如下:
private void RunContinuations(object continuationObject) // separated out of FinishContinuations to enable it to be inlined
{
bool canInlineContinuations =
(m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
RuntimeHelpers.TryEnsureSufficientExecutionStack();
switch (continuationObject)
{
// Handle the single IAsyncStateMachineBox case. This could be handled as part of the ITaskCompletionAction
// but we want to ensure that inlining is properly handled in the face of schedulers, so its behavior
// needs to be customized ala raw Actions. This is also the most important case, as it represents the
// most common form of continuation, so we check it first.
case IAsyncStateMachineBox stateMachineBox:
AwaitTaskContinuation.RunOrScheduleAction(stateMachineBox, canInlineContinuations);
LogFinishCompletionNotification();
return;
// Handle the single Action case.
case Action action:
AwaitTaskContinuation.RunOrScheduleAction(action, canInlineContinuations);
LogFinishCompletionNotification();
return;
// Handle the single TaskContinuation case.
case TaskContinuation tc:
tc.Run(this, canInlineContinuations);
LogFinishCompletionNotification();
return;
// Handle the single ITaskCompletionAction case.
case ITaskCompletionAction completionAction:
RunOrQueueCompletionAction(completionAction, canInlineContinuations);
LogFinishCompletionNotification();
return;
}
}
卦中的 case 挺有意思的,除了本篇聊过的 TaskContinuation 和 IAsyncStateMachineBox 之外,还有另外两种 continuationObject,这里说一下 ITaskCompletionAction 是怎么回事,其实它是 Task.Result 的底层延续类型,所以大家应该能理解为什么 Task.Result 能唤醒,主要是得益于Task.m_continuationObject =completionAction 所致。
说了这么说,如何眼见为实呢?可以从源码中寻找答案。
private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
{
var mres = new SetOnInvokeMres();
AddCompletionAction(mres, addBeforeOthers: true);
var returnValue = mres.Wait(Timeout.Infinite, cancellationToken);
}
private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
{
internal SetOnInvokeMres() : base(false, 0) { }
public void Invoke(Task completingTask) { Set(); }
public bool InvokeMayRunArbitraryCode => false;
}
从卦中可以看到,其实就是把 ITaskCompletionAction 接口的实现类 SetOnInvokeMres 塞入了 Task.m_continuationObject 中,一旦Task执行完毕之后就会调用 Invoke() 下的 Set() 来实现事件唤醒。
四:总结
虽然异步任务延续有三种实现方法,但底层都是一个套路,即借助 Task.m_continuationObject 字段玩出的各种花样,当然他们也是有一些区别的,即对 m_continuationObject 任务是否用单独的线程调度,产生了不同的意见分歧。

聊一聊 C#异步 任务延续的三种底层玩法的更多相关文章
- [Android] Android 异步定时任务实现的三种方法(以SeekBar的进度自动实现为例)
[Android] Android 定时异步任务实现的三种方法(以SeekBar的进度自动实现为例) 一.采用Handler与线程的sleep(long)方法 二.采用Handler与timer及Ti ...
- C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法
有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...
- JS异步加载的三种方式
js加载的缺点:加载工具方法没必要阻塞文档,过得js加载会影响页面效率,一旦网速不好,那么整个网站将等待js加载而不进行后续渲染等工作. 有些工具方法需要按需加载,用到再加载,不用不加载,. 默认正常 ...
- JS异步加载的三种方案
js加载的缺点:加载工具方法没必要阻塞文档,个别js加载会影响页面效率,一旦网速不好,那么整个网站将等待js加载而不进行后续渲染等工作. 有些工具方法需要按需加载,用到再加载,不用不加载. 一.def ...
- JavaScript异步加载的三种方式——async和defer、动态创建script
一.script标签的位置 传统的做法是:所有script元素都放在head元素中,必须等到全部js代码都被下载.解析.执行完毕后,才能开始呈现网页的内容(浏览器在遇到<body>标签时才 ...
- js异步加载的三种解决方案
默认情况javascript是同步加载的,也就是javascript的加载时阻塞的,后面的元素要等待javascript加载完毕后才能进行再加载,对于一些意义不是很大的javascript,如果放在页 ...
- javascript异步加载的三种解决方案
默认情况javascript是同步加载的,也就是javascript的加载时阻塞的,后面的元素要等待javascript加载完毕后才能进行再加载,对于一些意义不是很大的javascript,如果放在页 ...
- .Net中集合排序的一种高级玩法
背景: 学生有名称.学号, 班级有班级名称.班级序号 学校有学校名称.学校编号(序号) 需求 现在需要对学生进行排序 第一排序逻辑 按学校编号(序号)排列 再按班级序号排列 再按学生学号排列 当然,在 ...
- JavaScript的三种工业化调试方法
JavaScript的三种工业化玩法 软件工程中任何的语言如果想要写出健壮的代码都需要锋利的工具,当然JavaScript也不例外,很多朋友刚入门的时候往往因为工具选的不对而事半功倍,JavaScri ...
- 三种数据库访问——Spring JDBC
本篇随笔是上两篇的延续:三种数据库访问——原生JDBC:数据库连接池:Druid Spring的JDBC框架 Spring JDBC提供了一套JDBC抽象框架,用于简化JDBC开发. Spring主要 ...
随机推荐
- games101_Homework6
实现 Ray-Bounding Volume 求交与 BVH 查找 在本次编程练习中,你需要实现以下函数: • IntersectP(const Ray& ray, const Vector3 ...
- 4.使用二进制方式搭建K8S集群
使用二进制方式搭建K8S集群 注意 [暂时没有使用二进制方式搭建K8S集群,因此本章节内容不完整... 欢迎小伙伴能补充~] 准备工作 在开始之前,部署Kubernetes集群机器需要满足以下几个条件 ...
- 从0搭建一个FIFO模块-01(基础知识)
1. FIFO介绍 基本概念 FIFO(First In, First Out)是一种常用的数据结构,用于存储和处理数据.它的工作原理与排队的顺序类似,遵循"先进先出"的原则.即, ...
- npm报错error:0308010C:digital envelope routines::unsupported
error:0308010C:digital envelope routines::unsupported 出现这个错误是因为 node.js V17版本中最近发布的OpenSSL3.0, 而Open ...
- (Python基础教程之四)Python中的变量的使用
Python基础教程 在SublimeEditor中配置Python环境 Python代码中添加注释 Python中的变量的使用 Python中的数据类型 Python中的关键字 Python字符串操 ...
- 反汇编动态调试器之x64dbg
转载:https://cloud.tencent.com/developer/article/2337843 x64dbg 是一款开源.免费.功能强大的动态反汇编调试器,它能够在Windows平台上进 ...
- python开发包之远程隧道链接sshtunnel
缘起: 公司很多的数据库的链接都是本地连接或者指定ip地址可以访问, 如果你没有该ip权限, 但是你可以登录该数据库所在的服务器, 这个时候就可以使用ssh链接上这个服务器,以此为跳板进行数据库的链接 ...
- 终于解决了.net在线客服系统总是被360误报的问题(对软件进行数字签名)
升讯威在线客服与营销系统是基于 .net core / WPF 开发的一款在线客服软件,宗旨是: 开放.开源.共享.努力打造 .net 社区的一款优秀开源产品. 背景 我在业余时间开发的这个客服系统, ...
- Python 调整Excel行高、列宽
在Excel中,默认的行高和列宽可能不足以完全显示某些单元格中的内容,特别是当内容较长时.通过调整行高和列宽,可以确保所有数据都能完整显示,避免内容被截断.合理的行高和列宽可以使表格看起来更加整洁和专 ...
- Mac文件拷贝Win后的._文件清理
前言 我们在从mac向win拷贝文件后总会多出来 部分 ._ 开头的文件或名为.DS_Store的文件 根据上图在苹果官方社区的回答来看,这些文件存储了主文件的一些资料,图表等数据,如果说未来这些文件 ...