(譯).NET4.X 並行任務中Task.Start()的FAQ


傳送門:異步編程系列目錄……

 

         近期有不少人向我咨詢關於TaskStart()方法。比如:何時使用及何時不使用Start()Start()又做了些什么……我想在這里回答一些問題試圖澄清和平息任何關於Start()方法是什么以及做了什么的誤解。

 

1.         問題:我什么時候能使用TaskStart()方法?

         只有Task處於TaskStatus.Created狀態時才能使用實例方法Start()。並且,只有在使用Task的公共構造函數構造的Task實例才能處於TaskStatus.Created狀態。

表示 Task 的生命周期中的當前階段。

    public enum TaskStatus
    { 
        // 該任務已初始化,但尚未被計划。
        Created = 0,
        // 該任務正在等待 .NET Framework 基礎結構在內部將其激活並進行計划。
        WaitingForActivation = 1,
        // 該任務已被計划執行,但尚未開始執行。
        WaitingToRun = 2,

        // 該任務正在運行,但尚未完成。
        Running = 3,
        // 該任務已完成執行,正在隱式等待附加的子任務完成。
        WaitingForChildrenToComplete = 4,

        // 已成功完成執行的任務。
        RanToCompletion = 5,
        // 該任務已通過對其自身的 CancellationToken 引發 OperationCanceledException 異常
        Canceled = 6,
        // 由於未處理異常的原因而完成的任務。
        Faulted = 7,
    }

 

2.         問題:使用Task.Run()/Task.ContinueWith()/Task.Factory.StartNew()/TaskCompletionSource/異步方法(即使用asyncawait關鍵字的方法)……應該調用Start()方法嗎?

不應該。不僅不應該,而且也不能,因為此時調用Start()會報異常。從問題1可知:Start()實例方法只適用於TaskStatus.Created狀態的Task。由上面提到的方式創建的Task其狀態不是TaskStatus.Created,而是如TaskStatus.WaitingForActivationTaskStatus.RunningTaskStatus.RanToCompletion

 

3.         問題:Start()方法實際做了什么?

Start()將任務排隊到目標TaskScheduler(無參的Start()重載任務調度者為TaskScheduler.Current)。當你使用Task的構造函數創建一個Task實例時,它處於創建狀態(TaskStatus.Created),它沒有與任何調度器關聯,也沒有真真被執行。如果你永遠不調用Start()方法,那么此任務永遠不會排隊也不會完成。為了讓任務被執行,它需要在調度器上進行排隊,以便調度器在合適的時刻執行它。在Task上調用Start()方法將改變任務內部的一些數據(eg:狀態從Created改變為WaitingToRun)並且將任務通過TaskScheduler實例的QueueTask()方法排隊到目標調度器。此時,此任務未來的執行掌握在調度器手中,最終會通過TaskSchedulerTryExecuteTask實例方法執行。

     // 表示一個處理將任務排隊到線程中的底層工作的對象。
     public abstract class TaskScheduler
     {
          protected TaskScheduler();
 
          // 獲取與當前正在執行的任務關聯的 TaskScheduler。
          public static TaskScheduler Current { get; }
          // 獲取由 .NET Framework 提供的默認 TaskScheduler 實例。
          public static TaskScheduler Default { get; }
          // 創建一個與當前 SynchronizationContext 關聯的 TaskScheduler。
          public static TaskScheduler FromCurrentSynchronizationContext();

          // 當出錯的 Task 的未觀察到的異常將要觸發異常升級策略時發生,默認情況下,這將終止進程。
          public static event EventHandler<UnobservedTaskExceptionEventArgs> UnobservedTaskException;

          // 獲取此 TaskScheduler 實例的唯一 ID。
          public int Id { get; }
          // 指示此 TaskScheduler 能夠支持的最大並發級別,默認計划程序返回 System.Int32.MaxValue。
          public virtual int MaximumConcurrencyLevel { get; }

          // 僅對於調試器支持,生成當前排隊到計划程序中等待執行的 Task 實例的枚舉。
          protected abstract IEnumerable<Task> GetScheduledTasks();
          // 將 Task 排隊到計划程序中。
          protected internal abstract void QueueTask(Task task);
          // 嘗試將以前排隊到此計划程序中的 Task 取消排隊。
          protected internal virtual bool TryDequeue(Task task);
          // 嘗試在此計划程序上執行提供的 Task。執行失敗的常見原因是,
          // 該任務先前已經執行或者位於正在由另一個線程執行的進程中。
          protected bool TryExecuteTask(Task task);
          // 確定提供的 Task是否可以在此調用中同步執行,如果可以,將執行該任務。
          //   taskWasPreviouslyQueued:
          //     一個布爾值,該值指示任務之前是否已排隊。如果此參數為 True,則該任務以前可能已排隊(已計划);
          //     如果為 False,則已知該任務尚未排隊,此時將執行此調用,以便以內聯方式執行該任務,而不用將其排隊。
          // 返回結果:
          //     一個布爾值,該值指示是否已以內聯方式執行該任務。
          protected abstract bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued);
}

 

4.         問題:我能在同一個Task上調用多次Start()方法嗎?

不行,Task只能離開創建狀態(TaskStatus.Created)一次,而Start()方法使Task離開創建狀態。因此,Start()方法只能使用一次。任何企圖在不是創建狀態的Task上調用Start()都將導致一個異常。Start()方法采用同步方式運行以確保任務對象保持一致的狀態,即使是同時調用多次Start(),也只可能有一個調用會成功。

 

5.         問題:使用Task.Start()與使用Task.Factory.StartNew()有何不同?

Task.Factory.StartNew()可快速創建一個Task並且開啟任務。代碼如下:

var t = Task.Factory.StartNew(someDelegate);

這等效於:

var t = new Task(someDelegate); 
t.Start();

表現方面,前者更高效。就像在問題3中提到的,Start()采用同步方式運行以確保任務對象保持一致的狀態即使是同時調用多次Start(),也可能只有一個調用會成功。相比之下,StartNew()知道沒有其他代碼能同時啟動任務,因為在StartNew()返回之前它不會將創建的Task引用給任何人,所以StartNew()不需要采用同步方式執行。

 

6.         問題:我聽說Task.Result屬性也能開啟任務,真的嗎?

不能,只有兩種方式可能使Task離開其創建狀態

1)         將一個CancellationToken傳遞給Task的構造函數,並且這個token已經或稍后請求取消。如果Task任然處於Created/WaitingForActivation/WaitingToRun,當token取消時,Task將改變為TaskStatus.Canceled狀態。

2)         Task上調用Start()

因此, Result屬性不能開啟任務。如果對處於創建狀態的Task實例上調用Wait()方法或Result屬性,這個調用將被阻塞,需要等待開啟任務,這樣它就可以排隊到調度器,調度程序最終會執行它,然后完成任務。被阻塞的調用就被喚醒。

         你可能會認為不是這樣的。Result能開啟任務,但是這只適用於“內聯”任務的執行。如果一個任務已經排隊到TaskScheduler,但是這個任務可能任然保持在調取器的任務隊列中。當你對被排隊的任務請求Result屬性時,運行時將嘗試內聯任務的執行而不是純粹的阻塞和等待調度器在未來某個時刻使用其他線程來完成任務執行。因此,調用Result屬性可能終止於TaskSchedulerTryExecuteTaskInline()方法的調用,並且如何處理請求由TaskScheduler決定。

 

7.         問題:我應該提供返回未啟動任務的公共APIs嗎?

更恰當的問題是“我應該提供處於創建狀態任務的公共APIs嗎”,答案是“不能”。

         基本原因是:當你正常調用同步方法,該方法將很快被調用執行。對於返回Task的方法,你可以把Task看做是異步方法完成的結果。但是這並不能改變需要調用Start()方法開始相關操作的事實。因此,返回一個處於創建狀態的Task的異步方法是很奇怪的,只是想代表一個沒有開始的操作?

         因此,如果你有一個返回Task的公共方法,並且Task是使用構造函數創建的,請確保你在返回Task之前開啟任務。否則,很可能在APIs使用方導致死鎖或類似的問題,因為使用方期待調用完成時Task也最終完成,但如果返回一個尚未啟動的任務,它將永遠不會完成。有些框架允許你參數化方法或委托,返回Task甚至驗證返回任務的狀態,如果Task還是創建狀態就為其拋出異常。

 

8.         問題:我應該使用Task的構造函數 + TaskStart()實例方法嗎?

在大多數情況下,你最好使用一些其他機制。比如,如果你只是想計划一個任務來運行你提供的委托,你最好使用Task.Run()靜態方法或Task.FactoryStartNew()實例方法,而不是使用Task的構造函數創建一個任務再調用Start()開啟它,這不僅僅減少了代碼量,並且更加高效(見問題5回答),另外還可以減少犯錯的可能,比如忘記開啟任務。

當然,在一些情況下使用Task的構造函數+Start()更加有意義。比如,需要根據某些原因來選擇傳遞而來的Task,然后再使用Start()方法來實際排隊任務。

另外,一個更明顯的示例是,如果你想獲得任務本身的引用,可能使用了如下代碼:

Task theTask = null; 
theTask = Task.Run(() => Console.WriteLine(“My ID is {0}.”, theTask.Id));

         有問題,存在競爭?在Task.Run()方法內部,會創建一個新Task對象並且將其排隊到線程池調度器中。如果線程池比較空閑,那么會立即分配一個輔助線程開始執行任務。新創建的任務最后會存儲在theTask變量中,輔助線程和調用Task.Run()的線程會發生競爭的訪問此變量。我們能解決這種競爭通過分離構造函數與TaskScheduler

Task theTask = null; 
theTask = new Task(() =>Console.WriteLine(“My ID is {0}.”, theTask.Id)); 
theTask.Start(TaskScheduler.Default);

         現在我們已經確保Task實例將在線程池執行任務之前被存儲到theTask變量。因為線程池在Task對象調用Start()排隊任務之前無法獲得Task對象的引用,並且在這個時候,變量theTask已經設置為Task的引用,與后面線程池訪問theTask.Id不會存在競爭問題。

 

推薦並行任務相關資源:《關於AsyncAwaitFAQ

 

================================================================================================

 

園友提醒:(.NET4.5對.NET4.0的並行任務進行過改進,然而我正式學習並行任務的時候已經是.NET4.5,所以對於新改進的API沒有進行整理了,這邊有園友提醒,做下記錄,方便大家。)

      @zhangweiwen(Task.Run()是.net4.5新提供的API)

 

================================================================================================

 

         Ok,看完此文,相信你對並行任務中關於任務開啟又有更深入的理解了,(*^_^*),喜歡還請多多推薦。

 

原文:http://blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx

作者:Stephen Toub

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM