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

相關文章
相關標籤/搜索