溫故之.NET 任務並行

這篇文章主要講解 .NET 的任務並行,與數據並行不一樣的是:數據並行以數據爲處理單元,而任務並行,則以任務(工做)爲單元安全

關於任務的理解,若是還有疑問,能夠參考以前的文章【溫故之.NET 異步】微信

任務並行基礎

若是咱們想要建立並行的任務,能夠經過 Parallel.Invoke 來實現。它能夠很方便的幫助咱們同時運行多個任務,以下異步

public static void WorkOne() {
    // 任務一
}
public static void WorkTwo() {
    // 任務二
}
Parallel.Invoke(WorkOne, WorkTwo);

// 咱們也能夠經過 Lambda 表達式這樣寫
Parallel.Invoke(
    () => {
        // 任務一
    }, () => {
        // 任務二
    }
);
複製代碼

藉助 Parallel.Invoke ,咱們只需表達想同時運行的操做,CLR 會處理全部線程調度的具體信息(包括將線程數量自動縮放至計算機上的內核數)性能

須要特別注意
TPL 在後臺建立的 Task 數量不必定與所提供的操做的數量相等。 由於 TPL 可能會針對操做的數量進行不一樣程度的優化學習

所以,對 Parallel.Invoke ,咱們能夠這樣理解(只是爲了理解方便,不表示其內部具體實現也是這樣的)優化

  • 分配一個具備 4 個線程的「線程池」(假設計算機處理器爲 4 核 4 線程)。或者根據指定的 ParallelOptions 中的 MaxDegreeOfParallelism 屬性來肯定具體數量
  • 採用 Task.Run 的方式運行每個任務。每執行一個任務,就從「線程池」中取一個空閒的線程。若是沒有多餘的空閒線程,則等待
  • 直處處理完全部的任務爲止

這也能夠理解爲對其內部實現的一個猜想。若是有興趣,可使用 .NET Refactor 看一下其源碼spa

若是程序有 UI 線程,且任務的建立從 UI 線程開始,那麼在使用方式上會有變化,以下代碼所示線程

Task.Run(() => {
    Parallel.Invoke(
    () => {
        // 任務一
    }, () => {
        // 任務二
    });
});
複製代碼

這對於其餘的並行(如數據並行)也是同樣的。
只要咱們須要從 UI 線程建立並行,就應該使用 Task.Run 來啓動它們。不然,頗有可能產生死鎖(通常出如今當並行代碼內部須要訪問 UI 的狀況下,其餘狀況我也暫時沒有遇到過)設計

若是咱們分不清當前建立並行的是 UI 線程仍是其餘類型的線程。咱們能夠統一使用 Task 的方式來啓動它們。反正在大部分狀況下,使用 Task 來啓動也不會形成什麼性能問題code

不過,若是咱們須要並行當即啓動,或者儘快啓動,使用 Task 來啓動可能就不太合適,在系統工做量比較重的狀況下,咱們也不清楚這個 Task 何時可以執行。
在這種場景下,咱們能夠新建一個 Thread 來作這件事。由於 ThreadTask 不一樣,Thread 不以任務爲單位,當咱們調用 Thread.Start() 的時候,線程就會當即執行。而 Task,當咱們調用 Task.Run 的時候,它須要接受 TPL 的調度(Task Scheduler)。所以,其執行時間就不肯定了

針對建立並行,有如下建議

  • 在不肯定建立並行的是 UI 線程仍是其餘線程時,使用 Task.Run 來啓動並行(如前面例子所示)
  • 在系統工做比較重的狀況下,若是但願並行可以當即啓動,咱們應該使用 Thread 的方式
  • 不然,在大多數狀況下,不管 PC 端、Web 端、仍是 WebApi 後臺,咱們使用 Task.Run 來啓動並行是比較好的方式

經過 Thread 方式啓動並行,示例以下

Thread thread = new Thread(() => {
    Parallel.Invoke(
    () => {
        Debug.WriteLine("Work 1");
    },() => {
        Debug.WriteLine("Work 2");
    });
});
thread.Start();
複製代碼

針對並行的建議

前面提到,在多處理器條件下,使用 Parallel 能夠顯著提高性能。但事物總有兩面性,所以仍是有一些坑須要咱們注意

  • 對於任務並行,若是任務間具備強關聯性(即有不少任務的執行依賴於其餘的任務或者多個任務之間存在資源共享)。我的不建議使用並行庫,由於在以往的經驗中,這樣的處理並無爲咱們帶來特別大的性能提高
  • Parallel.ForParallel.ForEach 以數據並行爲主;Parallel.Invoke 以任務並行爲主
  • 不要對循環進行過分並行化。所謂物極必反,過分的並行化,不但增長了管理的難度,線程間的同步以及最後各個分區的合併,都會對性能形成影響
  • 若是並行裏面的單次迭代的工做量較小,推薦使用 Partitioner 來手動的對源集合進行分塊
  • 避免在並行代碼塊內調用非線程安全的方法,就算是聲明爲線程安全的方法,也應該儘可能少的調用
  • 儘可能避免在 UI 線程上執行並行循環。也應儘可能避免在並行代碼中更新 UI,由於這有可能會產生數據損壞或死鎖
  • 在並行迭代中,咱們不該該假定每個迭代順序開始。好比有集合 [1,2,3,4,5,6,7,8],假設分爲 4 個分塊 [1,2]、[3,4]、[5,6]、[7,8],咱們不該該認爲 [1,2] 這個塊要比 [5,6] 這個塊先執行。理解這個很重要,能夠防止咱們寫出可能產生死鎖的代碼,示例如【示例A】所示

示例A

ManualResetEventSlim mre = new ManualResetEventSlim();
int processor = Environment.ProcessorCount;
var source = Enumerable.Range(0, processor * 100);
Parallel.ForEach(source, item => {
    if (item == processor) {
        mre.Set();
    } else {
        mre.Wait();
    }
});
複製代碼

對於這段代碼,就可能會(可能性很是大)發生死鎖。如前面【針對並行的建議】的最後一點所說,一樣地,此處咱們也沒法肯定 mre.Set()mre.Wait() 到底誰先執行


至此,這篇文章的內容講解完畢。

後話
最近看了一些書籍,決定不管什麼時候,凡是關注了個人朋友,都一概關注回去
源於如下一點:尊重是相互的,學習也是相互的

在此,也感謝在微信公衆號、知乎、簡書、掘金等內容平臺關注個人朋友。歡迎關注公衆號【嘿嘿的學習日記】,全部的文章,都會在公衆號首發,Thank you~

公衆號二維碼
相關文章
相關標籤/搜索