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. C# ASP JS引用路径不正确导致的错误

    假设JS包放在根目录下的Scripts文件夹下 当前页的路径在另一个文件夹下,那么他引用JS应该:<script src="../Scripts/jquery-1.4.1.min.js ...

  2. java Hibernate 用法

    Hibernate 用法总结: import java.io.Serializable; import java.sql.SQLException; import java.util.Collecti ...

  3. Flutter 容器 (2) - Padding

    Padding: 内边距Widget,与CSS中的padding相似. import 'package:flutter/material.dart'; class AuthList extends S ...

  4. 简单认识Adam优化器

    转载地址 https://www.jianshu.com/p/aebcaf8af76e 基于随机梯度下降(SGD)的优化算法在科研和工程的很多领域里都是极其核心的.很多理论或工程问题都可以转化为对目标 ...

  5. HTML基础-03

    盒子模型 盒子模型(框模型 box model) - 浏览器在渲染页面时,它会将页面中的每一个元素都想象成是一个矩形的盒子. - 想象成盒子以后,对于页面的布局就变成了如何摆放盒子 - 每一个盒子从内 ...

  6. sharedb结合elementUi编写的实时小工具

    我是使用sharedb 作为后端 ,然后前端使用的elementUI样式,编写的一个值班小工具.接下来,让我们先来了解一下sharedb是什么吧? sharedb工具 github地址:https:/ ...

  7. 精讲RestTemplate第9篇-如何通过HTTP Basic Auth认证

    本文是精讲RestTemplate第9篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...

  8. Python 用DataFrame读 存 excel

    读 代码: import pandas as pd e = r'D:\pywork\12\excel信息表.xlsx' df = pd.DataFrame(pd.read_excel(e)) 存 D. ...

  9. 目录扫描、Nmap

    一.基本定义 1.目录扫描: 扫描站点的目录,寻找敏感文件(目录名.探针文件.后台.robots.txt.备份文件等). 2.目录:站点结构,权限控制不严格. 3.探针文件:服务器配置信息,例:php ...

  10. GridBagConstraints详解

    名称 作用 默认值 常量 位置 gridx 行(x)的第一个单元格 0并且为非负数 RELATIVE(相对的) 紧跟前一个组件的后面 gridy 列(y)的第一个单元格 0并且为非负数 RELATIV ...