.NET的併發編程(TPL編程)是什麼?

寫在前面

       優秀軟件的一個關鍵特徵就是具備併發性。過去的幾十年,咱們能夠進行併發編程,可是難度很大。之前,併發性軟件的編寫、調試和維護都很難,這致使不少開發人員爲圖省事放棄了併發編程。新版 .NET 中的程序庫和語言特徵,已經讓併發編程變得簡單多了。隨着 Visual Studio 2012 的發佈,微軟明顯下降了併發編程的門檻。之前只有專家才能作併發編程,而今天,每個開發人員都可以(並且應該)接受併發編程。html

解答疑問:.NET Core 同步和異步的差異數據庫

public ActionResult PushFileData([FromBody] Web_PushFileData file) //同步
public async ActionResult PushFileData([FromBody] Web_PushFileData file) //異步
疑問:對於同步方法,每一個請求都是使用同個線程嗎?如客戶A請求同步Action,還未執行完畢時,客戶B請求會阻塞。
對於異步方法,每一個請求都是從線程池拿空閒線程出來執行方法?也就是客戶A和客戶B請求方法,都是在不一樣子線程裏分別執行的。編程

導航

基本概念windows

  • 併發編程
  • TPL

線程基礎api

  • windows爲何要支持線程
  • 線程開銷
  • CPU的發展
  • 使用線程的理由

如何寫一個簡單Parallel.For循環數組

  • 數據並行
  • Parallel.For剖析

數據和任務並行中潛在的缺陷promise

  • 不要假設並行老是很快
  • 避免寫入共享緩存
  • 避免調用非線程安全的方法

       許多我的電腦和工做站都有多核CPU,能夠同時執行多個線程。爲了充分利用硬件,您能夠將代碼並行化,以便跨多個處理器分發工做。緩存

       在過去,並行須要對線程和鎖進行低級操做。Visual Studio和.NET框架經過提供運行時、類庫類型和診斷工具來加強對並行編程的支持。這些特性是在.NET Framework 4中引入的,它們使得並行編程變得簡單。您能夠用天然的習慣用法編寫高效、細粒度和可伸縮的並行代碼,而無需直接處理線程或線程池。安全

下圖展現了.NET框架中並行編程體系結構。
在這裏插入圖片描述服務器


1 基本概念

1.1 併發編程

併發

同時作多件事情

       這個解釋直接代表了併發的做用。終端用戶程序利用併發功能,在輸入數據庫的同時響應用戶輸入。服務器應用利用併發,在處理第一個請求的同時響應第二個請求。只要你但願程序同時作多件事情,你就須要併發。

多線程

       併發的一種形式,它採用多個線程來執行程序。從字面上看,多線程就是使用多個線程。多線程是併發的一種形式,但不是惟一的形式。

並行處理

把正在執行的大量的任務分割成小塊,分配給多個同時運行的線程。

       爲了讓處理器的利用效率最大化,並行處理(或並行編程)採用多線程。當現代多核 CPU行大量任務時,若只用一個核執行全部任務,而其餘覈保持空閒,這顯然是不合理的。

       並行處理把任務分割成小塊並分配給多個線程,讓它們在不一樣的核上獨立運行。並行處理是多線程的一種,而多線程是併發的一種。

異步編程

併發的一種形式,它採用 future 模式或回調(callback)機制,以免產生沒必要要的線程。

       一個 future(或 promise)類型表明一些即將完成的操做。在 .NET 中,新版 future 類型
有 Task 和 Task 。在老式異步編程 API 中,採用回調或事件(event),而不是
future。異步編程的核心理念是異步操做:啓動了的操做將會在一段時間後完成。這個操做
正在執行時,不會阻塞原來的線程。啓動了這個操做的線程,能夠繼續執行其餘任務。當
操做完成時,會通知它的future,或者調用回調函數,以便讓程序知道操做已經結束。

       NOTE:一般狀況下,一個併發程序要使用多種技術。大多數程序至少使用了多線程(經過線程池)和異步編程。要大膽地把各類併發編程形式進行混合和匹配,在程序的各個部分使用
合適的工具。

1.2 TPL

       任務並行庫(TPL)是System.Threading和System.Threading.Tasks命名空間中的一組公共類型和API。

       TPL動態地擴展併發度,以最有效地使用全部可用的處理器。經過使用TPL,您能夠最大限度地提升代碼的性能,同時專一於您的代碼的業務實現。

從.NET Framework 4開始,TPL是編寫多線程和並行代碼的首選方式。

2 線程基礎

2.1 Windows 爲何要支持線程

       在計算機的早期歲月,操做系統沒提供線程的概念。事實上,整個系統只運行着一個執行線程(單線程),其中同時包含操做系統代碼和應用程序代碼。只用一個執行線程的問題在於,長時間運行的任務會阻止其餘任務執行。
例如,在16位Windows的那些日子裏,打印一個文檔的應用程序很容易「凍結」整個機器,形成OS和其餘應用程序中止響應。有的程序含有bug,會形成死循環。遇到這個問題,用戶只好重啓計算機。用戶對此深惡痛絕。

       因而微軟下定決心設計一個新的OS,這個OS必須健壯,可靠,易因而伸縮以安全,同同時必須改進16位windows的許多不足。

       微軟設計這個OS內核時,他們決定在一個進程(Process)中運行應用程序的每一個實例。進程不過是應用程序的一個實例要使用的資源的一個集合。每一個進程都被賦予一個虛擬地址空間,確保一個進程使用的代碼和數據沒法由另外一個進程訪問。這就確保了應用程序實例的健壯性。因爲應用程序破壞不了其餘應用程序或者OS自己,因此用戶的計算體驗變得更好了。

       聽起來彷佛不錯,但CPU自己呢?若是一個應用程序進入無限循環,會發生什麼呢?若是機器中只有一個CPU,它會執行無限循環,不能執行其它任何東西。因此,雖然數據沒法被破壞,並且更安全,但系統仍然可能中止響應。微軟要修復這個問題,他們拿出的方案就是線程。做爲Windows概念,線程的職責是對CPU進行虛擬化。Windows爲每一個進程都提供了該進程專用的專用的線程(功能至關於一個CPU,可將線程理解成一個邏輯CPU)。若是應用程序的代碼進入無限循環,與那個代碼關聯的進程會被「凍結」,但其餘進程(他們有本身的線程)不會凍結:他們會繼續執行!

2.2 線程開銷

       線程是一個很是強悍的概念,由於他們使windows即便在執行長時間運行的任務時也能隨時響應。另外,線程容許用戶使用一個應用程序(好比「任務管理器」)強制終止彷佛凍結的一個應用程序(它也有可能正在執行一個長時間運行的任務)。可是,和一切虛擬化機制同樣,線程會產生空間(內存耗用)和時間(運行時的執行性能)上的開銷。

       建立線程,讓它進駐系統以及最後銷燬它都須要空間和時間。另外,還須要討論一下上下文切換。單CPU的計算機一次只能作一件事情。因此,windows必須在系統中的全部線程(邏輯CPU)之間共享物理CPU。

       在任何給定的時刻,Windows只將一個線程分配給一個CPU。那個線程容許運行一個時間片。一旦時間片到期,Windows就上下文切換到另外一個給線程。每次上下文切換都要求Windows執行如下操做:

  • 將CPU寄存器中的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中。
  • 從現有線程集合中選一個線程供調度(切換到的目標線程)。若是該線程由另外一個進程擁有,Window在開始執行任何代碼或者接觸任何數據以前,還必須切換CPU「看得見」的虛擬地址空間。
  • 將所選上下文結構中的值加載到CPU的寄存器中。

       上下文切換完成後,CPU執行所選的線程,直到它的時間片到期。而後,會發生新一輪的上下文切換。Windows大約每30ms執行一次上下文切換。

       上下文切換是淨開銷:也就是說上下文切換所產生的開銷不會換來任何內存或性能上的收益。

       根據上述討論,咱們的結論是必須儘量地避免使用線程,由於他們要耗用大量的內存,並且須要至關多的時間來建立,銷燬和管理。Windows在線程之間進行上下文切換,以及在發生垃圾回收的時候,也會浪費很多時間。然而,根據上述討論,咱們還得出一個結論,那就是有時候必須使用線程,由於它們使Windows變得更健壯,反應更靈敏。

       應該指出的是,安裝了多個CPU或者一個多核CPU)的計算機能夠真正同時運行幾個線程,這提高了應用程序的可伸縮性(在少許的時間裏作更多工做的能力)。Windows爲每一個CPU內核都分配一個線程,每一個內核都本身執行到其餘線程的上下文切換。Windows確保單個線程不會在多個內核上同時被調度,由於這會代理巨大的混亂。今天,許多計算機都包含了多個CPu,超線程CPU或者多核CPU。可是,windows最初設計時,單CPU計算機纔是主流,因此Windows設計了線程來加強系統的響應能力和可靠性。今天,線程還被用於加強應用程序的可伸縮性,但在只有多CPU(或多核CPU)計算機上纔有可能發生。

TIP:一個時間片結束時,若是Windows決定再次調度同一個線程(而不是切換到另外給一個線程),那麼Windows不會執行上下文切換。線程將繼續執行,這顯著改進了性能。設計本身的代碼時注意,上下文切換能避免的就要儘可能避免。

2.3 CPU的發展

       過去,CPU速度一直隨着時間在變快。因此,在一臺舊機器上運行得慢的程序在新機器上通常會快些。然而,CPU 廠商沒有延續CPU愈來愈快的趨勢。因爲CPU廠商不能作到一直提高CPU的速度,因此它們側重於將晶體管作得愈來愈小,使一個芯片上可以容納更多的晶體管。今天,一個硅芯片能夠容納2個或者更多的CPU內核。這樣一來,若是在寫軟件時能利用多個內核,軟件就能運行得更快些。

今天的計算機使用瞭如下三種多CPU技術。

  1. 多個CPU
  2. 超線程芯片
  3. 多核芯片

2.4 使用線程的理由

使用線程有如下三方面的理由。

  1. 使用線程能夠將代碼同其餘代碼隔離
           這將提升應用程序的可靠性。事實上,這正是Windows在操做系統中引入線程概念的緣由。Windows之因此須要線程來得到可靠性,是由於你的應用程序對於操做系統來講是的第三方組件,而微軟不會在你發佈應用程序以前對這些代碼進行驗證。若是你的應用程序支持加載由其它廠商生成的組件,那麼應用程序對健壯性的要求就會很高,使用線程將有助於知足這個需求。
  2. 可使用線程來簡化編碼
           有的時候,若是經過一個任務本身的線程來執行該任務,或者說單獨一個線程來處裏該任務,編碼會變得更簡單。可是,若是這樣作,確定要使用額外的資源,也不是十分「經濟」(沒有使用盡可能少的代碼達到目的)。如今,即便要付出一些資源做爲代價,我也寧願選擇簡單的編碼過程。不然,乾脆堅持一直用機器語言寫程序好了,徹底不必成爲一名C#開發人員。但有的時候,一些人在使用線程時,以爲本身選擇了一種更容易的編碼方式,但實際上,它們是將事情(和它們的代碼)大大複雜化了。一般,在你引入線程時,引入的是要相互協做的代碼,它們可能要求線程同步構造知道另外一個線程在何時終止。一旦開始涉及協做,就要使用更多的資源,同時會使代碼變得更復雜。因此,在開始使用線程以前,務必肯定線程真的可以幫助你。
  3. 可使用線程來實現併發執行
           若是(並且只有)知道本身的應用程序要在多CPU機器上運行,那麼讓多個任務同時運行,就能提升性能。如今安裝了多個CPU(或者一個多核CPU)的機器至關廣泛,因此設計應用程序來使用多個內核是有意義的。

3 數據並行(Data Parallelism)

3.1 數據並行

       數據並行是指對源集合或數組中的元素同時(即並行)執行相同操做的狀況。在數據並行操做中,源集合被分區,以便多個線程能夠同時在不一樣的段上操做。

數據並行性是指對源集合或數組中的元素同時任務並行庫(TPL)經過system.threading.tasks.parallel類支持數據並行。這個類提供了for和for each循環的基於方法的並行實現。

您爲parallel.for或parallel.foreach循環編寫循環邏輯,就像編寫順序循環同樣。您沒必要建立線程或將工做項排隊。在基本循環中,您沒必要使用鎖。底層工做TPL已經幫你處理。

下面代碼展現順序和並行:

// Sequential version            
foreach (var item in sourceCollection)
{
    Process(item);
}

// Parallel equivalent
Parallel.ForEach(sourceCollection, item => Process(item));
 
並行循環運行時,TPL對數據源進行分區,以便循環能夠同時在多個部分上運行。在後臺,任務調度程序根據系統資源和工做負載對任務進行分區。若是工做負載變得不平衡,調度程序會在多個線程和處理器之間從新分配工做。

下面的代碼來展現如何經過Visual Studio調試代碼:

public static void test()
        {
            int[] nums = Enumerable.Range(0, 1000000).ToArray();
            long total = 0;
            
            // Use type parameter to make subtotal a long, not an int
            Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
            {
                subtotal += nums[j];
                return subtotal;
            },
                (x) => Interlocked.Add(ref total, x)
            );

            Console.WriteLine("The total is {0:N0}", total);
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }
  • 選擇調試 > 開始調試,或按F5。
  • 應用在調試模式下啓動,並會在斷點處暫停。
  • 在中斷模式下打開線程經過選擇窗口調試 > Windows > 線程。 您必須位於一個調試會話以打開或請參閱線程和其餘調試窗口。
    在這裏插入圖片描述

3.2 Parallel.For剖析

查看Parallel.For的底層,

public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
 

清楚的看到有個func函數,看起來很熟悉。

 [TypeForwardedFrom("System.Core, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089")]
    public delegate TResult Func<out TResult>();
 

原來是定義的委託,有多個重載,具體查看文檔:https://docs.microsoft.com/en-us/dotnet/api/system.func-4?view=netframework-4.7.2

實際上TPL以前,實現併發或多線程,基本都要使用委託。

TIP:關於委託,你們能夠查看(https://docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/delegates)。或者《細說委託》(https://www.cnblogs.com/laoyu/archive/2013/01/13/2859000.html)

4 數據和任務並行中潛在的缺陷

       在許多狀況下,parallel.for和parallel.foreach能夠比普通的順序循環提供顯著的性能改進。然而,並行循環的工做引入了複雜性,這可能會致使在順序代碼中不常見或根本不會遇到的問題。本主題列舉了一些實踐來幫您避免這些問題,當你在寫並行代碼的時候。

4.1 不要假設並行老是很快

       在某些狀況下,並行循環的運行速度可能比其順序等效循環慢。基本的經驗法則是,具備不多迭代和快速用戶委託的並行循環不太可能加快速度。可是,因爲有不少因素會影響性能,我建議您測量實際結果。

4.2 避免寫入共享緩存

       在順序代碼中,讀寫靜態變量或者字段是很正常的。然而,每當多個線程同時訪問這些變量時,就有很大的競爭條件潛力。即便您可使用鎖來同步對變量的訪問,同步成本也會損害性能。所以,咱們建議您儘量避免或至少限制對並行循環中共享狀態的訪問。最好的方式是使用Parallel.For 和 Parallel.ForEach的重載方法,在並行循環期間,它們使用System.Threading.ThreadLocal泛型類型的變量來存儲線程本地狀態。經過使用並行循環,您將產生劃分源集合和同步工做線程的開銷。並行化的好處進一步受到計算機上處理器數量的限制。在一個處理器上運行多個計算綁定線程並不能加快速度。所以,要注意不要過分使用並行。

過分使用並行最多見的場景發生在嵌套循環中。在大多數狀況下,最好僅在外層循環使用並行,除非如下幾種場景適用:

  • 內層循環很長
  • 您正在對每筆訂單執行昂貴的計算。
  • 目標系統有足夠的處理器來處理經過並行處理對客戶訂單的查詢而產生的線程數。

在全部狀況下,肯定最佳查詢形狀的最佳方法都是測試和度量。

4.3 避免調用非線程安全的方法

       從並行循環中寫入非線程安全的實例方法可能會致使數據損壞,這在程序中可能會被檢測到,也可能不會被檢測到。它可能致使異常。在如下示例中,多線程會嘗試同時調用FileStream.WriteByte方法,可是這個是不被支持的。

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
 

參考文獻:

  1. https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/
  2. https://docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/delegates
  3. https://www.cnblogs.com/laoyu/archive/2013/01/13/2859000.html
  4. 《C#併發經典實例》
  5. 《CLR via C#》第3版
  6. https://www.52interview.com/solutions/38

 

歡迎關注訂閱個人微信公衆平臺【熊澤有話說】,更多好玩易學知識等你來取
做者:熊澤-學習中的苦與樂
公衆號:熊澤有話說
當前出處: https://www.cnblogs.com/xiongze520/p/14271739.html
原文出處: https://www.52interview.com/solutions/38

創做不易,版權歸做者和博客園共有,轉載或者部分轉載、摘錄,請在文章明顯位置註明做者和原文連接。  
相關文章
相關標籤/搜索