一 併發編程簡介 1.1 關於併發和並行 併發和並行的概念: 併發:(Concurrent),在某個時間段內,如果有多個任務執行,即有多個線程在操作時,如果系統只有一個CPU,則不能真正同時進行一個以上的線程, 它只能把CPU運行時間劃分成若幹個時間段,再將時間段分配給各個線程執行,在一個時間段的線 ...
一 併發編程簡介
1.1 關於併發和並行
併發和並行的概念:
- 併發:(Concurrent),在某個時間段內,如果有多個任務執行,即有多個線程在操作時,如果系統只有一個CPU,則不能真正同時進行一個以上的線程,
它只能把CPU運行時間劃分成若幹個時間段,再將時間段分配給各個線程執行,在一個時間段的線程代碼運行時,其它線程處於掛起狀態。
- 並行:(Parallel),當系統有一個以上CPU時,則線程的操作有可能非併發。
當一個CPU執行一個線程時,另一個CPU可以執行另一個線程,兩個線程互不搶占CPU資源,可以同時進行。
併發和並行是即相似又有區別的兩個概念:
- 併發是指兩個或多個事件在同一時間間隔內發生;
- 並行是指兩個或者多個事件在同一時刻發生。
1.2 關於併發編程和並行編程
併發編程的形式:
- 多線程編程:使用多個線程執行程式;
- 響應式編程。
- 非同步編程:APM,EAP,TAP。推薦使用 async await關鍵字的TAP ( Task-based Asynchronous Pattern );
並行編程:把正在執行的大量任務分割成小塊,分配個多個同時運行的線程,讓它們在不同的核上獨立運行。讓處理器的利用效率最大化。
二 非同步編程簡介
2.1 async await關鍵字
現代的非同步.Net程式使用兩個關鍵字,async和await。async關鍵字加在方法聲明上,它的主要目的是使方法內的await關鍵字生效。
如果async方法有返回值,應返回Task<T>,如果沒有返回值,應返回Task。Task類型相當於future,用來在非同步方法結束時通知調用程式。
一個非同步方法例子:
async Task MethodAsync() { var i = 10; await Task.Delay(1000); i++; await Task.Delay(1000); Trace.WriteLine(i); }
async方法在開始時以同步方式執行。在async方法內部,await關鍵字對它的參數執行一個非同步等待。
它首先檢測操作是否已經完成,如果完成了,就繼續以同步方式運行,否則會暫停方法,並返回,留下一個未完成的Task。
一段時間後,操作完成,async方法就恢復,從await出繼續往下執行。
2.2 ConfigureAwait ( )
一個 async 方法是由多個同步執行的程式塊組成的,每個同步程式塊之間由 await 語句分隔。
第一個同步程式塊在調用這個方法的線程中運行,那await之後的同步程式塊在哪個線程運行呢?
一個常見的情況是,用 await 語句等待一個任務完成,當該方法在 await 處暫停時,就可以捕捉上下文(context)。
如果當前 SynchronizationContext 不為空,這個上下文就是當前SynchronizationContext。如果當前 SynchronizationContext 為空,則這個上下文為當前TaskScheduler。
該方法會在這個上下文中繼續運行。一般來說,運行 UI 線程時採用 UI 上下文,處理 ASP.NET 請求時採用 ASP.NET 請求上下文,其他很多情況下則採用線程池上下文。
因此,在上面的代碼中,每個同步程式塊會試圖在原始的上下文中恢復運行。如果在 UI線程中調用 MethodAsync,這個方法的每個同步程式塊都將在此 UI 線程上運行。但
是,如果線上程池線程中調用,每個同步程式塊將線上程池線程上運行。
要控制這種行為,可以在 await 中使用 ConfigureAwait 方法,將參數 continueOnCapturedContext設為 false。接下來的代碼剛開始會在調用的線程里運行,在被 await 暫停後,
則會線上程池線程里繼續運行。
async Task MethodAsync() { //在調用線程運行 var i = 10; await Task.Delay(1000).ConfigureAwait(false); //線上程池線程運行 i++; await Task.Delay(1000); Trace.WriteLine(i); }
2.3 異常處理
使用async await時,自然要處理異常。一旦非同步方法拋出異常,該異常會放在返回的 Task 對象中,並且這個 Task對象的狀態變為“已完成”。
當 await 調用該 Task 對象時,await 會獲得並(重新)拋出該異常,並且保留著原始的棧軌跡。
async Task MethodAsync() { //發生異常時,任務結束,不會直接拋出異常。 var task = ThrowExceptionAsync(); try { //await時,拋出異常 await task; } catch (Exception ex) { Trace.WriteLine(ex.StackTrace); } } async Task ThrowExceptionAsync() { await Task.Delay(1000); throw new Exception("ThrowExceptionAsync"); }
2.4 一旦在代碼中使用了非同步,最好一直使用
關於非同步方法,還有一條重要的準則:一旦在代碼中使用了非同步,最好一直使用。調用非同步方法時,應該(在調用結束時)用 await 等待它返回的 task 對象。
一定要避免使用Task.Wait 或 Task<T>.Result 方法,因為它們會導致死鎖。參考一下下麵這個方法:
void Deadlock() { // 開始延遲 Task task = WaitAsync(); // 同步程式塊,正在等待非同步方法完成 task.Wait(); } async Task WaitAsync() { // 這裡 awati 會捕獲當前上下文 await Task.Delay(TimeSpan.FromSeconds(1)); // 這裡會試圖用上面捕獲的上下文繼續執行 }
如果從 UI 或 ASP.NET 的上下文調用這段代碼,就會發生死鎖。這是因為,這兩種上下文每次只能運行一個線程。Deadlock 方法調用 WaitAsync 方法,WaitAsync 方法開始調用delay 語句。
然後,Deadlock 方法(同步)等待 WaitAsync 方法完成,同時阻塞了上下文線程。當 delay 語句結束時,await 試圖在已捕獲的上下文中繼續運行 WaitAsync 方法,但這個步驟無法成功,
因為上下文中已經有了一個阻塞的線程,並且這種上下文只允許同時運行一個線程。
這裡有兩個方法可以避免死鎖:
在 WaitAsync 中使用 ConfigureAwait(false)(導致 await 忽略該方法的上下文),
或者用 await 語句調用 WaitAsync 方法(讓 Deadlock變成一個非同步方法)。
三 並行編程簡介
如果程式中有大量的計算任務,並且這些任務能分割成幾個互相獨立的任務塊,可以考慮使用並行編程。並行編程可臨時提高 CPU 利用率,以提高吞吐量。
並行的形式有兩種:
-
數據並行(data parallelism):有大量的數據需要處理,並且每一塊數據的處理過程基本上是彼此獨立的。
-
任務並行(task parallelim):需要執行大量任務,並且每個任務的執行過程基本上是彼此獨立的。任務並行可以是動態的,如果一個任務的執行結果會產生額外的任務,這些新增的任務也可以加入任務池。
3.1 數據並行
實現數據並行有幾種不同的做法。
一種做法是使用 Parallel.ForEach 方法,它類似於foreach 迴圈,應儘可能使用這種做法。
Parallel 類也提供 Parallel.For 方法,這類似於 for 迴圈,當數據處理過程基於一個索引時,可使用這個方法。
另一種做法是使用 PLINQ(Parallel LINQ), 它為 LINQ 查詢提供了 AsParallel 擴展。
與PLINQ 相比,Parallel 對資源更加友好,Parallel 與系統中的其他進程配合得比較好 , 而PLINQ 會試圖讓所有的 CPU 來執行本進程。
//Parallel.ForEach void RotateMatrices(IEnumerable<Matrix> matrices, float degrees) { Parallel.ForEach(matrices, matrix => matrix.Rotate(degrees)); } //PLINQ IEnumerable<bool> PrimalityTest(IEnumerable<int> values) { return values.AsParallel().Select(val => IsPrime(val)); }
不管選用哪種方法,在並行處理時有一個非常重要的準則。只要任務塊是互相獨立的,並行性就能做到最大化。
一旦在多個線程中共用狀態,就必須以同步方式訪問這些狀態,那樣程式的並行性就變差了。
有多種方式可以控制並行處理的輸出。可以把結果存在某些併發集合,或者對結果進行聚合。3.2 任務並行
任務並行關註執行任務,Parallel 類的 Parallel.Invoke 方法可以以 分叉 / 聯合”(fork/join)的方式的使任務並行。
調用該方法時,把要並行執行的委托(delegate)作為傳入參數:
void ProcessArray(double[] array) { Parallel.Invoke( () => ProcessPartialArray(array, 0, array.Length / 2), () => ProcessPartialArray(array, array.Length / 2, array.Length) ); } void ProcessPartialArray(double[] array, int begin, int end) { // CPU 密集型的操作…… }
任務並行也強調任務塊的獨立性。委托(delegate)的獨立性越強,程式的執行效率就越高。
四 併發編程的集合
併發編程所用到的集合有兩類:
- 併發集合;
- 不可變集合。
4.1 併發集合
多個線程可以用安全的方式同時更新併發集合。大多數併發集合使用快照snapshot),
當一個線程在增加或刪除數據時,另一個線程也能枚舉數據。比起給常規集合加鎖以保護數據的方式,採用併發集合的方式要高效得多。
4.2 不可變集合
不可變集合實際上是無法修改的。要修改一個不可變集合,需要建立一個新的集合來代表這個被修改了的集合。