1. 程式人生 > >C#併發程式設計之初識並行程式設計

C#併發程式設計之初識並行程式設計

寫在前面

之前微信公眾號裡有一位叫sara的朋友建議我寫一下Parallel的相關內容,因為手中商城的重構工作量較大,一時之間無法抽出時間。近日,這套系統已有階段性成果,所以準備寫一下Parallel的相關內容,正好也延續之前的C#併發程式設計系列。

Parallel是並行程式設計的相關內容,而Parallel.For和Parallel.Foreach又是並行程式設計中相當重要的方法,所以不能孤立的去討論Parallel,必須要放到並行程式設計的討論中去。

並行化,一般是對所要完成的任務進行劃分,並且以併發的方式處理屬於自己的那份任務,並且最終可以做到整合,所以並行化總會產生併發性。

實際上並行是併發的子集,併發和並行都可以是多執行緒執行,就看其處理器是否是多核的,就看這些執行緒能不能同時被cpu多個核執行,如果可以就說明是並行,而併發是多個執行緒被cpu單核輪流切換著執行。總之,只有在多核處理器上並行才會有意義。

並行化總會有著很大的挑戰,即每一個部分以不同順序或者交錯執行,都能保證最終結果的正確性,尤其涉及到各並行部分之間需要序列執行的部分,這個挑戰是很大的。由於並行化程式設計要比普通的序列程式碼複雜很多,也難維護很多,所以不是所有的問題都可以使用並行的。比如絕對執行時間本來就很少,即使使用併發可以提高整體的執行時間,那麼我們也應該使用傳統方式。但是如果主要涉及到提升使用者響應能力的功能,那麼我們推薦使用並行程式設計,同時處理分割後依然可以獨立進行而不影響整體任務的功能也可以使用並行程式設計。

並行的相關實戰

說到並行,就需要先說下.NET FX4中引入的Task Parallel Library(任務並行庫),簡稱TPL。TPL主要覆蓋了三大使用場景,資料並行、任務並行和流水線,TPL以其高度的封裝特性,隱藏了並行程式設計裡複雜的處理,使得開發人員可以以較低的門檻進行並行程式設計。

資料並行

這種場景在於有大量資料需要處理,而且對每一份資料都要執行的同樣的操作。

任務並行

有很多相對獨立的不同操作,或者可以分割成多個子任務但彼此之間是獨立的,可以通過任務並行來發揮並行化的優勢

流水線

流水線是以上兩種場景的結合,這個也是最複雜最難處理的場景,因為這裡面涉及到多個併發的任務進行協調處理。

此場景,奈何小編理解的不是很好,所以不敢亂寫,多方查詢資料,找到了oschina上的一篇文章。

流水線技術,指的是允許一個機器週期內的計算機各處理步驟重疊進行。特別是,當執行一條指令時,可以讀取下一條指令,也就意味著,在任何一個時刻可以有不止一條指令在“流水線”上,每條指令處在不同的執行階段。這樣,即便讀取和執行每條指令的時間保持不變,而計算機的總的吞吐量提高了。


原文地址:https://my.oschina.net/u/3374461/blog/1930305

System.Threading.Tasks.Parallel類

雖然Parallel類在System.Threading.Tasks名稱空間下,但是建立並行程式碼不一定要直接使用Task類的例項,事實上我們可以直接使用Parallel靜態類所提供的方法。

Parallel.For:為固定數目的獨立For迴圈迭代提供了負載均衡式的並行執行

   1:  Parallel.For(0, 5, i =>
   2:  {
   3:      Console.WriteLine("the number is", i);
   4:  });

Parallel.Foreach:為固定數目的獨立ForEach迴圈迭代提供了負載均衡式的並行執行。這個方法支援自定義分割槽器(Partitioner),以使得我們截圖完全掌控資料分發。

   1:  string[] letters = new string[] {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"};
   2:  Parallel.ForEach(letters, i => Console.WriteLine("letter is " + i));

Parallel.Invoke:為給定的獨立任務提供了負載均衡式的並行執行,接下來會重點討論這個方法。

Parallel.Invoke

這個方法很實用,也很簡單。

以下程式碼可以返回void的無引數方法:

   1:  Parallel.Invoke(Method1(), Method2(), Method3(), Method4());

通過Lambda表示式執行:

   1:  Parallel.Invoke(() => Method1(), () => Method2(), () => Method3(), () => Method4());

通過Lambda表示式和匿名型別來執行:

   1:  Parallel.Invoke(() =>
   2:  {
   3:      Method1();
   4:      // Do something
   5:  }, 
   6:  () =>
   7:  {
   8:      Method2();
   9:      // Do something
  10:  }, 
  11:  () =>
  12:  {
  13:      Method3();
  14:      // Do something
  15:  }, 
  16:  () =>
  17:  {
  18:      Method4();
  19:      // Do something
  20:  });

以上程式碼需要並行執行四個方法,但是如果空餘邏輯核心不足四個或者根本就沒有四個邏輯核心,這四個方法是不能併發執行的。因此在理想情況下,正好有至少四個空餘邏輯核心時,我們就可以並行執行這四個方法了。

這四個方法,我們無法準確的預測其執行順序,因為這一切是由底層的邏輯會根據執行時的現有可用資源創建出最合適的執行計劃。當然TPL依然有機制保證方法的順序執行,這個以後我們再討論。

Parallel.Invoke最大的優勢就是簡單,但是並不能因為它簡單,就不分場合的使用,事實上,我們需要在某些場景下權衡使用。

  • 如果這四個方法的執行時間不一致,那麼就需要根據最長的執行時間才能返回控制,這就可能造成一些邏輯核心處於閒置狀態。所以我們需要預測一下大致的執行時間,如果時間過長,那麼就要認真考慮是否真的需要使用這個方法。
  • 其擴充套件性很差,因為它只能呼叫固定數目的邏輯核心,剩餘核心就會一直處於閒置狀態。
  • 方法之間的互動極其困難,極易產生Bug,當然這是並行程式設計的常見問題,TPL也考慮到了這點,也有足夠機制解決這個問題。
  • 如果其中某個方法有了異常,捕捉異常會很困難,所以需要大家在相應的被呼叫方法裡編寫足夠的日誌。
  • 小編在以前的使用中還遇到了記憶體溢位的異常,這些也會在以後的文章中說明其原因以及解決方法。

今天就寫到這兒吧,已經十二點了,要休息一下,保護頭髮了,哈哈