寫在前面
優秀軟體的一個關鍵特征就是具有并發性,過去的幾十年,我們可以進行并發編程,但是難度很大,以前,并發性軟體的撰寫、除錯和維護都很難,這導致很多開發人員為圖省事放棄了并發編程,新版 .NET 中的程式庫和語言特征,已經讓并發編程變得簡單多了,隨著 Visual Studio 2012 的發布,微軟明顯降低了并發編程的門檻,以前只有專家才能做并發編程,而今天,每一個開發人員都能夠(而且應該)接受并發編程,
解答疑問:.NET Core 同步和異步的差別
public ActionResult PushFileData([FromBody] Web_PushFileData file) //同步
public async ActionResult PushFileData([FromBody] Web_PushFileData file) //異步
疑問:對于同步方法,每個請求都是使用同個執行緒嗎?如客戶A請求同步Action,還未執行完畢時,客戶B請求會阻塞,
對于異步方法,每個請求都是從執行緒池拿空閑執行緒出來執行方法?也就是客戶A和客戶B請求方法,都是在不同子執行緒里分別執行的,
導航
基本概念
- 并發編程
- TPL
執行緒基礎
- windows為什么要支持執行緒
- 執行緒開銷
- CPU的發展
- 使用執行緒的理由
如何寫一個簡單Parallel.For回圈
- 資料并行
- Parallel.For剖析
資料和任務并行中潛在的缺陷
- 不要假設并行總是很快
- 避免寫入共享快取
- 避免呼叫非執行緒安全的方法
許多個人電腦和作業站都有多核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技術,
- 多個CPU
- 超執行緒芯片
- 多核芯片
2.4 使用執行緒的理由
使用執行緒有以下三方面的理由,
- 使用執行緒可以將代碼同其他代碼隔離
這將提高應用程式的可靠性,事實上,這正是Windows在作業系統中引入執行緒概念的原因,Windows之所以需要執行緒來獲得可靠性,是因為你的應用程式對于作業系統來說是的第三方組件,而微軟不會在你發布應用程式之前對這些代碼進行驗證,如果你的應用程式支持加載由其它廠商生成的組件,那么應用程式對健壯性的要求就會很高,使用執行緒將有助于滿足這個需求, - 可以使用執行緒來簡化編碼
有的時候,如果通過一個任務自己的執行緒來執行該任務,或者說單獨一個執行緒來處里該任務,編碼會變得更簡單,但是,如果這樣做,肯定要使用額外的資源,也不是十分“經濟”(沒有使用盡量少的代碼達到目的),現在,即使要付出一些資源作為代價,我也寧愿選擇簡單的編碼程序,否則,干脆堅持一直用機器語言寫程式好了,完全沒必要成為一名C#開發人員,但有的時候,一些人在使用執行緒時,覺得自己選擇了一種更容易的編碼方式,但實際上,它們是將事情(和它們的代碼)大大復雜化了,通常,在你引入執行緒時,引入的是要相互協作的代碼,它們可能要求執行緒同步構造知道另一個執行緒在什么時候終止,一旦開始涉及協作,就要使用更多的資源,同時會使代碼變得更復雜,所以,在開始使用執行緒之前,務必確定執行緒真的能夠幫助你, - 可以使用執行緒來實作并發執行
如果(而且只有)知道自己的應用程式要在多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]));
參考文獻:
- https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/
- https://docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/delegates
- https://www.cnblogs.com/laoyu/archive/2013/01/13/2859000.html
- 《C#并發經典實體》
- 《CLR via C#》第3版
- https://www.52interview.com/solutions/38
歡迎關注訂閱我的微信公眾平臺【熊澤有話說】,更多好玩易學知識等你來取作者:熊澤-學習中的苦與樂 公眾號:熊澤有話說 當前出處: https://www.cnblogs.com/xiongze520/p/14271739.html 原文出處: https://www.52interview.com/solutions/38 創作不易,著作權歸作者和博客園共有,轉載或者部分轉載、摘錄,請在文章明顯位置注明作者和原文鏈接,
|
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/248366.html
標籤:.NET技术
下一篇:IoC依賴注入分析
