TaskContinuationsOptions.ExecuteSynchronously探秘
TPL - Task Parallel Library为我们提供了Task相关的api,供我们非常方便的编写并行代码,而不用自己操作底层的Thread类。使用Task的优势是显而易见的:
提供返回值
异常捕获
节省Context Switch造成的开销
另一个Task带来的优势就是不再需要通过阻塞线程来等待Task结束,如果需要在Task结束时开启另一项任务,可以使用Task.ContinueWith这个方法,并传入一个指定的委托即可。而本文主要关注ContinueWith中的TaskContinuationsOptions参数中的ExecuteSynchronously这个枚举值
ExecuteSynchronously是什么
我们先来看一下官方文档对于ExecuteSynchronously给出的解释
Specifies that the continuation task should be executed synchronously. With this option specified, the continuation runs on the same thread that causes the antecedent task to transition into its final state. If the antecedent is already complete when the continuation is created, the continuation will run on the thread that creates the continuation. If the antecedent's CancellationTokenSource is disposed in a finally block (Finally in Visual Basic), a continuation with this option will run in that finally block. Only very short-running continuations should be executed synchronously.
一大长串,我们尝试解析一下这一堆话在说什么。首先,当调用者传入这个枚举值后,意味着ContinueWith中传入的委托将会在原Task的同一线程上执行,但要注意的是,这里的同一线程指的是:将原Task转移到final state的线程。因为原Task的执行可能涉及了多个线程,因此这里特意指明是final state对应的线程,而不是从所有涉及的线程中随机挑选一个。
其次,如果调用ContinueWith的时候,原Task已经执行完毕,那么continue的委托并不会在刚才提到的那个final state对应的线程上执行,而是由创建这个continuation的线程执行。
最后一点,如果原Task的CancellationTokenSource在finally块中调用了Dispose方法,那么continue的委托就会在那个finally块中执行。(其实这一点我也没有理解到底是什么意思,欢迎大神拍砖)
举个例子
class Program
{
static void Main(string[] args)
{
for (int i = ; i < ; i++)
{
Task.Run(async () =>
{
Console.WriteLine($"Running on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay();
});
}
Task t = Task.Run(async () =>
{
Console.WriteLine($"=======Running on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay();
Console.WriteLine($"=======Running on thread {Thread.CurrentThread.ManagedThreadId}");
}); // Thread.Sleep(5000);
t.ContinueWith(_ =>
{
Console.WriteLine($"*******Running on thread {Thread.CurrentThread.ManagedThreadId}");
}, TaskContinuationOptions.ExecuteSynchronously); Console.ReadLine();
}
}
这段代码首先创建了30个干扰Task,这样能显著降低即使不用ExecuteSynchronously,线程池也会分配原线程来执行Continue任务的概率。运行后发现,任务t和continue确实是在同一个线程上执行的。而注释掉TaskContinuationOptions.ExecuteSynchronously后,continue就会由线程池重新分配线程。而如果取消注释线程Sleep 5秒这行代码,即使ExecuteSynchronously,continue也会由线程池重新分配线程执行,这正如上一段文档中提到的:调用ContinueWith时,如果原任务已经执行完毕,那么会由调用ContinueWith的线程执行continue任务,在这里就会由主线程来执行continue任务。
ExecuteSynchronously为什么不是默认行为
微软工程师Stephen Toub在其一篇博文中解释了为什么.NET团队没有把ExecuteSynchronously作为默认方案。
一个Task任务有可能会多次调用ContinueWith方法,如果默认是在同一线程执行,那么所有的continue任务都需要等待上一个continue完成后才能执行,这也就失去了并行的意义。
还有一种常见的情况就是很多个continue任务一个接一个的串在一起,如果这些continue任务都是同步顺序执行的,一个任务完成了就会执行下一个任务,这将导致线程栈上堆积的frame越来越多,这有可能会导致线程栈溢出。
为了解决溢出的问题,通常的解决方式是借用一个“蹦床”,把需要完成的工作在当前线程栈之外保存起来,然后利用一个更高level的frame检索存储的任务并执行。这样一来,每次完成一个任务之后,并不是立即执行下一个任务,而是将其保存至上述的frame并退出,该frame将执行下一个任务。而TPL正是利用这一方式来提升异步的执行效率。
以上就是没有默认同步运行任务的主要原因,虽然性能上会稍有损失,但这样可以更好的利用并行,更安全,而这性能的损失通常来说并不是最重要的。作者最后也建议我们如果Task里的语句很简单的话,同步执行也是值得的。正如官方文档最后一句提到的:
Only very short-running continuations should be executed synchronously.
如果是一个复杂又耗时的任务以同步方式来执行的话就有点得不偿失了。
ExecuteSynchronously在什么情况下不会同步执行
Stephen Toub提到,即使在调用ContinueWith的时候传入了TaskContinuationOptions.ExecuteSynchronously,CLR也只能尽量让continue在原Task线程上执行,但无法100%保证。
如果原Task的线程被Abort,那么与其关联的continue任务是无法在原线程上执行的。
在上一段中我们也提到了关于线程栈溢出的问题,如果TPL认为接着在该线程上运行continue任务有溢出的风险,continue任务就会转而变成异步执行。
最后一种情况就是Task Scheduler不允许同步执行Task,开发者可以自定义一个TaskScheduler,重写父类方法,决定任务的执行方式。
最后欢迎关注我的个人公众号:SoBrian,期待与大家共同交流,共同成长!

Reference
https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcontinuationoptions
https://devblogs.microsoft.com/pfxteam/why-is-taskcontinuationsoptions-executesynchronously-opt-in/
https://devblogs.microsoft.com/pfxteam/when-executesynchronously-doesnt-execute-synchronously/
TaskContinuationsOptions.ExecuteSynchronously探秘的更多相关文章
- C#服务器获取客户端IP地址以及归属地探秘
背景:博主本是一位Windows桌面应用程序开发工程师,对网络通信一知半解.一日老婆逛完某宝,问:"为什么他们知道我的地址呢,他们是怎么获取我的地址的呢?" 顺着这个问题我们的探秘 ...
- Composer概述及其自动加载探秘
composer概述 一开始,最吸引我的当属 Composer 了,因为之前从没用过 Composer . Composer 是PHP中用来管理依赖关系的工具,你只需在自己的项目中声明所依赖的外部工具 ...
- 基于AngularJS的个推前端云组件探秘
基于AngularJS的个推前端云组件探秘 AngularJS是google设计和开发的一套前端开发框架,帮助开发人员简化前端开发的负担.AngularJS将帮助标准化的开发web应用结构并且提供了针 ...
- .NET跨平台之旅:探秘 dotnet run 如何运行 .NET Core 应用程序
自从用 dotnet run 成功运行第一个 "Hello world" .NET Core 应用程序后,一直有个好奇心:dotnet run 究竟是如何运行一个 .NET Cor ...
- ASP.Net WebForm温故知新学习笔记:二、ViewState与UpdatePanel探秘
开篇:经历了上一篇<aspx与服务器控件探秘>后,我们了解了aspx和服务器控件背后的故事.这篇我们开始走进WebForm状态保持的一大法宝—ViewState,对其刨根究底一下.然后,再 ...
- ASP.Net WebForm温故知新学习笔记:一、aspx与服务器控件探秘
开篇:毫无疑问,ASP.Net WebForm是微软推出的一个跨时代的Web开发模式,它将WinForm开发模式的快捷便利的优点移植到了Web开发上,我们只要学会三步:拖控件→设属性→绑事件,便可以行 ...
- 探秘Tomcat——连接器和容器的优雅启动
前言: 上篇<探秘Tomcat——启动篇>粗线条的介绍了在tomcat在启动过程中如何初始化Bootstrap类,加载并执行server,从而启动整个tomcat服务,一直到我们看到控制台 ...
- 探秘Tomcat——启动篇
tomcat作为一款web服务器本身很复杂,代码量也很大,但是模块化很强,最核心的模块还是连接器Connector和容器Container.具体请看下图: 从图中可以看出 a. 高亮的两块是Conne ...
- 探秘Tomcat——从一个简陋的Web服务器开始
前言: 无论是之前所在实习单位小到一个三五个人做的项目,还是如今一个在做的百人以上的产品,一直都能看到tomcat的身影.工作中经常遇到的操作就是启动和关闭tomcat服务,或者修改了摸个java文件 ...
随机推荐
- 我搭的神经网络不work该怎么办!看看这11条新手最容易犯的错误
1. 忘了数据规范化 2. 没有检查结果 3. 忘了数据预处理 4. 忘了正则化 5. 设置了过大的批次大小 6. 使用了不适当的学习率 7. 在最后一层使用了错误的激活函数 8. 网络含有不良梯度 ...
- 存储系列之 VFS虚拟文件系统简介
引言:文件系统发展到一定阶段,开始进一步抽象和分层. 前面我们介绍了ext系列文件系统和xfs文件系统,这些是Linux使用最多的文件系统,也是很多发布版本默认选择的文件系统.而事实上,Linux ...
- 微信小程序之蓝牙广播信息
期初第一次做蓝牙开锁的时候遇到的最尖锐的问题就是ios设备如何对获取的广播信息进行读取,大概用了4中方式,都无法解决,最后不得不求助官方人员.给了一个方法,大家可以参考.在此附图: 由于mac地址是6 ...
- TS数据流PAT和PMT分析
TS流,是基于packet的位流格式,每个packet是188个字节或者204个字节(一般是188字节,204字节格式是在188字节的packet后面加上16字节的CRC数据,其他格式相同),解析TS ...
- SSM框架环境搭建
SSM基础环境搭建 创建maven工程 next,finish,等待创建完成,创建完成后,src/main下只有webapp文件夹,我们需要手动创建java和resources,鼠标右击main,ne ...
- MySQL数据库下统计记录数小于指定数值的数据
楼主在做一个智慧工地的产品,需要对工人进行一些数据统计,比如要统计导入人员数量小于30的工地,SQL应该怎么写呢? 首先了解一下数据结构,工地分三张表,四级层级关系,Organization表存储区域 ...
- 操作系统-进程(3)Linux下的进程相关命令
操作系统给予这个内存中的单元一个标识符(PID)依据登入者的UID/GID(/etc/passwd) 衍生出的其它程序(子程序),一般情况也,也会沿用这个程序(父程序)的相关权限 ParentID(P ...
- df卡死和fork:cannot allocate memory报错
早上到了公司,发现docker资源池的某一台主机根文件系统写满. 检查后发现该主机/data目录未挂载文件系统,直接放在了根目录下.于是联系业务方将应用迁移,联系主机工程师为/data挂载80G的存储 ...
- CentOS 7/8上部署Ceph
Ceph是一个分布式的存储系统,可以在统一的系统中提供唯一的对象.块和文件存储,Ceph的大致组件如下: 1. Ceph监视器(ceph-mon):用来维护集群状态的映射,包括监视器映射,管理器映射, ...
- fprintfAndFscanf简单操作
C语言fscanf和fprintf函数的用法详解(格式化读写文件)(代码转载) #include<stdio.h> #define N 2 struct stu{ char name[10 ...