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作为默认方案。

  1. 一个Task任务有可能会多次调用ContinueWith方法,如果默认是在同一线程执行,那么所有的continue任务都需要等待上一个continue完成后才能执行,这也就失去了并行的意义。

  2. 还有一种常见的情况就是很多个continue任务一个接一个的串在一起,如果这些continue任务都是同步顺序执行的,一个任务完成了就会执行下一个任务,这将导致线程栈上堆积的frame越来越多,这有可能会导致线程栈溢出。

  3. 为了解决溢出的问题,通常的解决方式是借用一个“蹦床”,把需要完成的工作在当前线程栈之外保存起来,然后利用一个更高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%保证。

  1. 如果原Task的线程被Abort,那么与其关联的continue任务是无法在原线程上执行的。

  2. 在上一段中我们也提到了关于线程栈溢出的问题,如果TPL认为接着在该线程上运行continue任务有溢出的风险,continue任务就会转而变成异步执行。

  3. 最后一种情况就是Task Scheduler不允许同步执行Task,开发者可以自定义一个TaskScheduler,重写父类方法,决定任务的执行方式。

最后欢迎关注我的个人公众号:SoBrian,期待与大家共同交流,共同成长!

Reference

TaskContinuationsOptions.ExecuteSynchronously探秘的更多相关文章

  1. C#服务器获取客户端IP地址以及归属地探秘

    背景:博主本是一位Windows桌面应用程序开发工程师,对网络通信一知半解.一日老婆逛完某宝,问:"为什么他们知道我的地址呢,他们是怎么获取我的地址的呢?" 顺着这个问题我们的探秘 ...

  2. Composer概述及其自动加载探秘

    composer概述 一开始,最吸引我的当属 Composer 了,因为之前从没用过 Composer . Composer 是PHP中用来管理依赖关系的工具,你只需在自己的项目中声明所依赖的外部工具 ...

  3. 基于AngularJS的个推前端云组件探秘

    基于AngularJS的个推前端云组件探秘 AngularJS是google设计和开发的一套前端开发框架,帮助开发人员简化前端开发的负担.AngularJS将帮助标准化的开发web应用结构并且提供了针 ...

  4. .NET跨平台之旅:探秘 dotnet run 如何运行 .NET Core 应用程序

    自从用 dotnet run 成功运行第一个 "Hello world" .NET Core 应用程序后,一直有个好奇心:dotnet run 究竟是如何运行一个 .NET Cor ...

  5. ASP.Net WebForm温故知新学习笔记:二、ViewState与UpdatePanel探秘

    开篇:经历了上一篇<aspx与服务器控件探秘>后,我们了解了aspx和服务器控件背后的故事.这篇我们开始走进WebForm状态保持的一大法宝—ViewState,对其刨根究底一下.然后,再 ...

  6. ASP.Net WebForm温故知新学习笔记:一、aspx与服务器控件探秘

    开篇:毫无疑问,ASP.Net WebForm是微软推出的一个跨时代的Web开发模式,它将WinForm开发模式的快捷便利的优点移植到了Web开发上,我们只要学会三步:拖控件→设属性→绑事件,便可以行 ...

  7. 探秘Tomcat——连接器和容器的优雅启动

    前言: 上篇<探秘Tomcat——启动篇>粗线条的介绍了在tomcat在启动过程中如何初始化Bootstrap类,加载并执行server,从而启动整个tomcat服务,一直到我们看到控制台 ...

  8. 探秘Tomcat——启动篇

    tomcat作为一款web服务器本身很复杂,代码量也很大,但是模块化很强,最核心的模块还是连接器Connector和容器Container.具体请看下图: 从图中可以看出 a. 高亮的两块是Conne ...

  9. 探秘Tomcat——从一个简陋的Web服务器开始

    前言: 无论是之前所在实习单位小到一个三五个人做的项目,还是如今一个在做的百人以上的产品,一直都能看到tomcat的身影.工作中经常遇到的操作就是启动和关闭tomcat服务,或者修改了摸个java文件 ...

随机推荐

  1. JDK 1.8 中文 API CHM

    链接: https://pan.baidu.com/s/1AiJn6RM1KoEL1n_96qoQhQ 提取码: n2ya

  2. web安全之python延时注入

    通过python代码编写的一个延时的sql注入脚本 首先我们导入了request请求库和string类型的库,通过库我们可以通过访问请求的方式访问url链接. url链接为注入链接地址这里我随便写的一 ...

  3. python设计模式之外观模式

    python设计模式之外观模式 系统会随着演化变得非常复杂,最终形成大量的(并且有时是令人迷惑的)类和交互,这种情况并不少见.许多情况下,我们并不想把这种复杂性暴露给客户端.外观设计模式有助于隐藏系统 ...

  4. GaussDB基本操作

    列出所有数据库 \l 切换数据库 \c ${databaseName} 列出当前数据库下的表 \d 列出表的所有字段 \d ${tableName} 查看指定表的基本情况 \d+ ${tableNam ...

  5. python 去除Excel中的重复行数据

    导入pandas import pandas as pd 1.读取excel中的数据: frame = pd.DataFrame(pd.read_csv('excel的绝对路径.csv'', 'She ...

  6. SpringMVC修改视图定位

    @ 目录 什么是视图定位 修改springmvc-servlet.xml 修改IndexController 移动index.jsp 测试 什么是视图定位 如果代码写成这样,就表示跳转到页面 inde ...

  7. Jmeter 常用函数(23)- 详解 __longSum

    如果你想查看更多 Jmeter 常用函数可以在这篇文章找找哦 https://www.cnblogs.com/poloyy/p/13291704.htm 作用 计算两个或多个长值的和 注意 当值不在 ...

  8. MySQL SQL概述

    MySQL SQL概述 数据库的好处: •实现数据持久化 •使用完整的管理系统统一管理易 数据库的概念: DB:数据库(database):存储数据的“仓库”.它保存了一系列有组织的数据. DBMS: ...

  9. CentOS下删除物理磁盘,删除LVM

    1.删除 dmsetup remove LV_name 2.vgreduce VG_name --removemissing 3.vgremove VG_name 4.pvremove disk

  10. 清空ARP缓存

    arp -n|awk '/^[1-9]/{print "arp -d " $1}'|sh -x