TaskContinuationsOptions.ExecuteSynchronously探秘

TPL - Task Parallel Library为咱们提供了Task相关的api,供咱们很是方便的编写并行代码,而不用本身操做底层的Thread类。使用Task的优点是显而易见的:api

  • 提供返回值安全

  • 异常捕获异步

  • 节省Context Switch形成的开销async

另外一个Task带来的优点就是再也不须要经过阻塞线程来等待Task结束,若是须要在Task结束时开启另外一项任务,可使用Task.ContinueWith这个方法,并传入一个指定的委托便可。而本文主要关注ContinueWith中的TaskContinuationsOptions参数中的ExecuteSynchronously这个枚举值性能

ExecuteSynchronously是什么

咱们先来看一下官方文档对于ExecuteSynchronously给出的解释this

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.spa

一大长串,咱们尝试解析一下这一堆话在说什么。首先,当调用者传入这个枚举值后,意味着ContinueWith中传入的委托将会在原Task的同一线程上执行,但要注意的是,这里的同一线程指的是:将原Task转移到final state的线程。由于原Task的执行可能涉及了多个线程,所以这里特地指明是final state对应的线程,而不是从全部涉及的线程中随机挑选一个。线程

其次,若是调用ContinueWith的时候,原Task已经执行完毕,那么continue的委托并不会在刚才提到的那个final state对应的线程上执行,而是由建立这个continuation的线程执行。code

最后一点,若是原Task的CancellationTokenSource在finally块中调用了Dispose方法,那么continue的委托就会在那个finally块中执行。(其实这一点我也没有理解究竟是什么意思,欢迎大神拍砖)blog

举个例子

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             for (int i = 0; i < 30; i++)
 6             {
 7                 Task.Run(async () =>
 8                 {
 9                     Console.WriteLine($"Running on thread {Thread.CurrentThread.ManagedThreadId}");
10                     await Task.Delay(2000);
11                 });
12             }
13             Task t = Task.Run(async () =>
14            {
15                Console.WriteLine($"=======Running on thread {Thread.CurrentThread.ManagedThreadId}");
16                await Task.Delay(2000);
17                Console.WriteLine($"=======Running on thread {Thread.CurrentThread.ManagedThreadId}");
18            });
19 
20             // Thread.Sleep(5000);
21             t.ContinueWith(_ =>
22             {
23                 Console.WriteLine($"*******Running on thread {Thread.CurrentThread.ManagedThreadId}");
24             }, TaskContinuationOptions.ExecuteSynchronously);
25 
26             Console.ReadLine();
27         }
28     }

 

这段代码首先建立了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

相关文章
相关标签/搜索