詳解.NET 4.0平行任務庫中必須注意的三種關係

來源:互聯網
上載者:User

      隨著.NET Framework v4.0的正式發布,  一個新的.NET程式世界開始徐徐拉開帷幕. 為應對迅速發展的硬體結構更新, 和各種軟體開發模式/思路/潮流的變化, 平行任務庫(Task Parallel Library, 簡稱TPL)成為.NET Framework v4.0各種新特性中最為閃耀的明星; 從社區的動向能明顯體現出對這一新特性的關注. 這個新特性的核心是Task, 底層支援是.NET 4.0 線程池的重新實現.

      在前幾個版本中, 微軟為.NET架構設計了線程池和最簡單的背景工作執行緒介面QueueUserWorkItem. 微軟認為這個API保持簡單易用, 是保證它能被廣泛使用的前提; 在.NET 4.0中, API的介面改為使用任務(Task)並為平行計算做出了調整, 但是這個API的使用依然保持了簡單這一準則, 如帶有get_首碼get_XXX的方法實為唯讀屬性XXX(可以通過VS2010開啟您的項目後, 依次點擊"Architecture" - "Create Dependency Graph", 選擇關係的組織方式, 就可以看到).:

 

上一版本線程池API的重要問題

      軟體計算最終要解決的是現實生活中的實際問題; 問題域不僅包含被計算的實體或者資料, 而且還包含這些實體和資料之間的關係. 計算或者演算法, 不僅需要處理問題域中的實體和資料, 還要通過計算過程隱含或明確地正確反映實體和資料之間的關係. 處理這些關係會最終體現在計算操作的關係上, 譬如, 時間先後, 因果相關, 層列組合, 整體局部等.

      在上一個版本的線程池應用程式介面(QueueUserWorkItem)中, 工作任務以獨立的工作項目的形式存在; 即應用程式介面假定程式開發人員投入到線程池中的都是獨立的執行單元, 線程池應用程式介面不提供任何輔助手段處理各個工作項目之間的關係. 微軟是出於什麼樣的考慮線上程池應用程式介面中如此保守我們不得而知, 但是這樣的限制帶來的明顯問題有:

      1. 程式開發人員責任加重. 毫無疑問, 正確處理資料及實體間的關係是演算法正確性的要求. 所以當線程池不提供處理關係的應用程式介面, 而多個工作項目之間確實存在需要處理的關係(比如某些操作要在另一些操作完成之後才能啟動)時, 開發人員只能在工作項目執行體中加入同步代碼才能達到這一目的. 這樣, 開發人員的責任加重, 程式的可讀性下降, 當大量這樣的代碼出現時, 系統的可維護性和品質就受到威脅;

      2. 工作項目執行的總體效率降低. 工作項目的有效載荷是工作項目需要完成的任務, 為執行工作項目而進行線程切換所花費的時間和空間是工作項目的"無效載荷". 我們當然期望有效載荷率越大越好, 但是當工作項目執行體非常小的時候, "無效載荷"對最終效能帶來的影響非常明顯. 在多核處理器架構下, 如果工作項目的總體數量很多, 遠遠超過處理器核心數-因為線程切換頻繁-的時候 實驗證明線程池的效能明顯降低. 在上一個版本中, 線程池應用介面認為工作項目之間是相互獨立的, 所以就無法隱含/顯式被告知或主動推斷工作項目之間的關係從而合并某些工作邏輯,也就達到降低無效載荷的目的.

      3. 開發方式受到影響. 由於API的這種限制, 開發人員更傾向於將"發射後不管"的任務放到線程池中執行, 而對於有同步或次序要求的任務, 更傾向於在獨立的線程中執行.

 

父子關係

      .NET Framework 4.0 新版本的線程池中, 使用平行任務庫作為主要的API. 平行任務庫帶來的第一個改變, 就是引入了父子關係. 父子關係簡單來講是一種組合關係, 又是一種依賴關係. 組合是指某些任務可以附加到某個任務上;這某個任務就成為父任務, 某些任務就成為和父任務關聯的子任務. 任務的這種父子關係本身不隱含任務大小劃分的粒度, 而在實踐中我們往往傾向於按照商務邏輯的組合關係將一個較大的任務劃分成許多可以單獨執行的子任務.

      依賴關係則是在父子關係中實實在在存在的. 在平行任務庫中, 每個任務有如下幾個狀態來描述:

  •           o Created
              o WaitingForActivation
              o WaitingToRun
              o Running
              o WaitingForChildrenToComplete
              o RanToCompletion
              o Canceled
              o Faulted

      其中最後三個是一個任務執行的終結狀態: RanToCompletion表示成功完成,Canceled表示任務被取消, Faulted表示該任務或其子任務拋出了異常. 在平行任務庫中, 因為多個子任務及父任務的異常可能有多個, 所以.NET 4.0中特別引入了新的異常類型AggregateException表示多個異常的彙總. Task類型的Exception屬性(如)就是一個AggregateException類型的執行個體. 父任務對子任務的依賴關係, 就體現在: 1). 只有當所有的子任務都執行完畢, 父任務才能到達終結狀態; 2). 只有到達終結狀態, 父任務的Exception屬性才會有具體的值(在之前一直保持null).使用如下的程式碼片段將某個任務註冊為父任務的子任務:

Create child task
  1.  Task parent = Task.Factory.StartNew(() =>
  2. {
  3.      Task child = Task.Factory.StartNew(() =>
  4.     {
  5.          //Child task execution body
  6.     }, TaskCreationOptions.AttachedToParent);
  7. });

      重點在StartNew方法的第二個參數上, 使用TaskCreationOptions.AttachedToParent告訴平行任務庫這個任務是建立為包含它的任務的子任務. 這裡特別需要注意的是:

      1. 雖然平行任務庫允許我們在某個任務中建立一個子任務並在另外一個任務中顯式地啟動這個子任務, 但是任務間的父子關係是在建立伊始就決定了的, 而不是在啟動時決定;
      2. 平行任務庫是如何為我們決定誰是父任務呢? 在建立伊始, 平行任務庫會在當前線程(注意!)中尋找正在執行的任務, 並將它作為父任務. 被建立的子任務會附加到這個父任務上.

          其實AggregateException本身也是特意為這種父子/組合的關係中出現的異常所設計的, 現在只用在平行任務庫及PLINQ中. 查看MSDN中的AggregateException Class.

     

    循序關聯性

          循序關聯性是為化簡一些簡單的同步操作而引入平行任務庫的重要改變. 實際計算問題中, 一定存在這樣的需求, 即某個問題的解決結果, 是開始對其他問題計算的先決條件(譬如某些演算法要求複用前一步的計算中間結果). 針對這樣的需求, 平行任務庫中存在動靜兩種方式, 來定義任務間的循序關聯性:靜態方式在建立任務時使用ContinueWith; 動態方式顯式使用Wait(); 下面的代碼集中樣本這兩種方式引入的循序關聯性:

    Task Sequence
    1.  private static void TaskSequenceDemo()
    2. {
    3.      Task parent = Task.Factory.StartNew(() =>
    4.     {
    5.          Task child1 = null, child2 = null, child3 = null, child4 = null;
    6.  
    7.         child1 = Task.Factory.StartNew(() =>
    8.         {
    9.             Console.WriteLine("The child task 1 says hello!");
    10.         }, TaskCreationOptions.AttachedToParent);
    11.  
    12.         child2 = child1.ContinueWith((t) =>
    13.         {
    14.             Console.WriteLine("The child task 2 says hello!");
    15.         }, TaskContinuationOptions.AttachedToParent);
    16.  
    17.         child3 = Task.Factory.StartNew(() =>
    18.         {
    19.             Task.WaitAll(new Task[2] { child4, child2 });
    20.             Console.WriteLine("The child task 3 says hello!");
    21.         }, TaskCreationOptions.AttachedToParent);
    22.  
    23.         child4 = Task.Factory.StartNew((t) =>
    24.         {
    25.             Console.WriteLine("The child task 4 says hello!");
    26.         }, TaskContinuationOptions.AttachedToParent);
    27.     });
    28. }

          除了WaitAll方法外, 另一個重要的方法是Wait(), 接受Timeout設定, 或者接受CancellationToken參數. 假設A是某個任務執行個體, A.Wait()方法表示調用者掛起並期待任務A執行完畢. 這就引出了.NET 4.0對於原有的線程池和應用介面的一個重大改進.

          從Wait看平行任務庫對於線程池應用介面的改進. 我們知道在上一個版本的線程池應用介面中, 我們需要自己處理同步需求. 譬如有兩個工作項目A和B, 在執行過程中都需要使用資源R1和R2. 如果我們的同步措施不當, 可能A佔有R1等待R2, B佔有R2等待R1, 從而工作項目A和B產生死結. 在最壞的情況下, 線程池中的所有線程可能都處於死結的狀態下而無法執行.

          在新的應用介面中, 平行任務庫為我們提供了Wait()方法. 這個方法絕不是簡單的類似於設定同步事件. 從各種文檔來看, A.Wait()方法最大的特性, 是檢查任務A的狀態. 如果A正在運行, 則等待A的完成; 如果A尚未啟動, 則調用A.Wait()方法的任務會將任務A引入到當前任務的執行線程中展開執行. 這在相當大程度上避免了因等待而產生死結的可能.

          各位可以通過簡單的實驗程式Wait()的task啟動並執行ManagedThreadId和ThreadId來驗證這一Inline過程。

     

    邏輯重疊關係

          這裡要解釋一下什麼是邏輯重疊關係, 因為這個詞是我杜撰出來的(囧). 所謂邏輯重疊, 即多個相同的執行體, 只是輸入和產生的輸出不同而已, 說白了就是ForXXX等類迴圈執行. NET 4.0特意為邏輯重疊關係作出了最佳化, 提供了Parallel.For(), Parallel.For<>()和Parallel.ForEach<>()方法.

          相對於已有的for和foreach只在一個線程中啟動並執行方式, 和將集合中的每個元素都單獨建立任務執行計算的方式, 一定需要一個平衡點和一種標準來衡量效率. 在前文提到, 先對於要完成的任務, 線程建立和切換是無效載荷. 引發線程切換的最大原因是windows的CPU調度和時間片分配方式. 所以CPU核心數就是這個標準和平衡點. 為了減少線程的切換, 一"堆"輸入值最好按照CPU的數目劃分開來並分配到CPU核心數個線程上,每個CPU核心運行一個線程. 理想狀態下, 計算的整個過程將沒有線程切換. Parallel.ForEach就可以智能的完成這一過程:

    ForEach
    1. Parallel.ForEach<int>(source, item =>
    2.     {
    3.         // execution body here
    4.     }
    5. )

    我們可以做一個實驗來驗證, 在foreach方式, Parallel.ForEach<>方式, 每個元素一個單獨的計算線程的方式下, 三者的效率對比. 不過由於關於這個關係的論述文章比較多, 相對的實驗也比較多. 在此處不再贅述.

     

    對新線程池的擔憂

          新的線程池實現了lock-free的任務提取和存入演算法, 使用了平行任務庫作為應用介面並著手處理任務之間的關係, 這一些都是顯著的改善; 新的任務被定義或期待被定義為細粒度能快速執行完成的執行單元, 這樣沒有問題. 但是在這樣的改善之後, 會不會出現線程池的誤用(比如依舊使用粗粒度任務長時間線上程池中活動), 過度使用和依賴, 我們拭目以待.

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.