1、關於併發編程的幾個誤解算法
1)併發就是多線程編程
實際上多線程只是併發編程的一種形式而已,在C#中還有不少其餘的併發編程技術,包括異步編程,並行編程,TPL數據流,響應式編程等。服務器
2)只有大型服務器才須要考慮併發多線程
服務器端的大型程序要響應大量客戶端的數據請求,固然要充分考慮併發。可是桌面程序和手機、平板等移動端應用一樣須要考慮併發編程,由於它們是直接面向最終用戶的,而如今用戶對使用體驗的要求愈來愈高。程序必須能隨時響應用戶的操做,尤爲是在後臺處理時(讀寫數據、與服務器通訊等),這正是併發編程的目的之一。併發
3)併發編程很複雜,必須掌握不少底層技術 異步
C# 和.NET 提供了不少程序庫,併發編程已經變得簡單多了。尤爲是.NET 4.5 推出了全新的async 和await 關鍵字,使併發編程的代碼減小到了最低限度。並行處理和異步開發已 經再也不是高手們的專利,每一個開發人員都能寫出交互性良好、高 效、可靠的併發程序。async
2、併發的幾個名稱術語異步編程
3、異步編程簡介 函數
異步編程有兩大好處。第一個好處是對於面向終端用戶的GUI 程序:異步編程提升了響應能力。咱們都遇到過在運行時會臨時鎖定界面的程序,異步編程可使程序在執行任務時仍能響應用戶的輸入。第二個好處是對於服務器端應用:異步編程實現了可擴展性。服務器應用能夠利用線程池知足其可擴展性,使用異步編程後,可擴展性一般能夠提升一個數量級。現代的異步.NET 程序使用兩個關鍵字:async 和await。async 關鍵字加在方法聲明上,它的主要目的是使方法內的await 關鍵字生效(爲了保持向後兼容,同時引入了這兩個關鍵字)。若是async 方法有返回值,應返回Task<T>;若是沒有返回值,應返回Task。這些task 類型至關於future,用來在異步方法結束時通知主程序。性能
我舉個例子:
1 async Task DoSomethingAsync() 2 { 3 int val = 13; 4 // 異步方式等待1 秒 5 await Task.Delay(TimeSpan.FromSeconds(1)); 6 val *= 2; 7 8 // 異步方式等待1 秒 9 await Task.Delay(TimeSpan.FromSeconds(1)); 10 Trace.WriteLine(val); 11 }
async 方法在開始時以同步方式執行。在async 方法內部,await 關鍵字對它的參數執行一個異步等待。它首先檢查操做是否已經完成,若是完成了,就繼續運行(同步方式)。不然,它會 暫停async 方法,並返回,留下一個未完成的task。一段時間後,操做完成,async 方法就恢復運行。
一個async 方法是由多個同步執行的程序塊組成的,每一個同步程序塊之間由await 語句分隔。第一個同步程序塊在調用這個方法的線程中運行,但其餘同步程序塊在哪裏運行呢?狀況比較複雜。最多見的狀況是,用await 語句等待一個任務完成,當該方法在await 處暫停時,就能夠捕捉上下文(context)。若是當前SynchronizationContext 不爲空,這個上下文就是當前SynchronizationContext。若是當前SynchronizationContext 爲空,則這個上下文爲當前TaskScheduler。該方法會在這個上下文中繼續運行。通常來講,運行UI 線程時採用UI 上下文,處理ASP.NET 請求時採用ASP.NET 請求上下文,其餘不少狀況下則採用線程池上下文。
有兩種基本的方法能夠建立Task 實例。有些任務表示CPU 須要實際執行的指令,建立這種計算類的任務時,使用Task.Run(如須要按照特定的計劃運行,則用TaskFactory.StartNew)。其餘的任務表示一個通知(notification),建立這種基於事件的任務時,使用TaskCompletionSource<T>。大部分I/O 型任務採用TaskCompletionSource<T>。
使用async 和await 時,天然要處理錯誤。在下面的代碼中,PossibleExceptionAsync 會拋出一個NotSupportedException 異常,而TrySomethingAsync 方法可很順利地捕捉到這個異常。這個捕捉到的異常完整地保留了棧軌跡,沒有人爲地將它封裝進TargetInvocationException 或AggregateException 類:
1 async Task TrySomethingAsync() 2 { 3 try 4 { 5 await PossibleExceptionAsync(); 6 } 7 catch(NotSupportedException ex) 8 { 9 LogException(ex); 10 throw; 11 } 12 }
一旦異步方法拋出(或傳遞出)異常,該異常會放在返回的Task 對象中,而且這個Task對象的狀態變爲「已完成」。當await 調用該Task 對象時,await 會得到並(從新)拋出該異常,而且保留着原始的棧軌跡。所以,若是PossibleExceptionAsync 是異步方法,如下代碼就能正常運行:
1 async Task TrySomethingAsync() 2 { 3 // 發生異常時,任務結束。不會直接拋出異常。 4 Task task = PossibleExceptionAsync(); 5 try 6 { 7 //Task 對象中的異常,會在這條await 語句中引起 8 9 await task; 10 } 11 catch(NotSupportedException ex) 12 { 13 LogException(ex); 14 throw; 15 } 16 }
關於異步方法,還有一條重要的準則:你一旦在代碼中使用了異步,最好一直使用。調用異步方法時,應該(在調用結束時)用await 等待它返回的task 對象。必定要避免使用Task.Wait 或Task<T>.Result 方法,由於它們會致使死鎖。參考一下下面這個方法:
1 async Task WaitAsync() 2 { 3 // 這裏awati 會捕獲當前上下文…… 4 await Task.Delay(TimeSpan.FromSeconds(1)); 5 // ……這裏會試圖用上面捕獲的上下文繼續執行 6 } 7 void Deadlock() 8 { 9 // 開始延遲 10 Task task = WaitAsync(); 11 // 同步程序塊,正在等待異步方法完成 12 task.Wait(); 13 }
若是從UI 或ASP.NET 的上下文調用這段代碼,就會發生死鎖。這是由於,這兩種上下文每次只能運行一個線程。Deadlock 方法調用WaitAsync 方法,WaitAsync 方法開始調用delay 語句。而後,Deadlock 方法(同步)等待WaitAsync 方法完成,同時阻塞了上下文線程。當delay 語句結束時,await 試圖在已捕獲的上下文中繼續運行WaitAsync 方法,但這個步驟沒法成功,由於上下文中已經有了一個阻塞的線程,而且這種上下文只容許同時運行一個線程。這裏有兩個方法能夠避免死鎖:在WaitAsync 中使用ConfigureAwait(false)(致使await 忽略該方法的上下文),或者用await 語句調用WaitAsync 方法(讓Deadlock變成一個異步方法)。
4、並行編程簡介
若是程序中有大量的計算任務,而且這些任務能分割成幾個互相獨立的任務塊,那就應該使用並行編程。並行編程可臨時提升CPU 利用率,以提升吞吐量,若客戶端系統中的CPU 常常處於空閒狀態,這個方法就很是有用,但一般並不適合服務器系統。大多數服務器自己具備並行處理能力,例如ASP.NET 可並行地處理多個請求。某些狀況下,在服務器系統中編寫並行代碼仍然有用(若是你知道併發用戶數量會一直是少數)。但一般狀況下,在服務器系統上進行並行編程,將下降自己的並行處理能力,而且不會有實際的好處。並行的形式有兩種:數據並行(data parallelism)和任務並行(task parallelim)。數據並行是指有大量的數據須要處理,而且每一塊數據的處理過程基本上是彼此獨立的。任務並行是指須要執行大量任務,而且每一個任務的執行過程基本上是彼此獨立的。任務並行能夠是動態的,若是一個任務的執行結果會產生額外的任務,這些新增的任務也能夠加入任務池。
實現數據並行有幾種不一樣的作法。一種作法是使用Parallel.ForEach 方法,它相似於foreach 循環,應儘量使用這種作法。
Parallel 類提供Parallel.For 和ForEach方法,這相似於for 循環,當數據處理過程基於一個索引時,可以使用這個方法。下面是使用Parallel.ForEach 的代碼例子:
1 void RotateMatrices(IEnumerable<Matrix> matrices, float degrees) 2 { 3 Parallel.ForEach(matrices, matrix => matrix.Rotate(degrees)); 4 }
另外一種作法是使用PLINQ(Parallel LINQ), 它爲LINQ 查詢提供了AsParallel 擴展。跟PLINQ 相比,Parallel 對資源更加友好,Parallel 與系統中的其餘進程配合得比較好, 而PLINQ 會試圖讓全部的CPU 來執行本進程。Parallel 的缺點是它太明顯。不少狀況下,PLINQ 的代碼更加優美。
1 IEnumerable<bool> PrimalityTest(IEnumerable<int> values) 2 { 3 return values.AsParallel().Select(val => IsPrime(val)); 4 }
無論選用哪一種方法,在並行處理時有一個很是重要的準則只要任務塊是互相獨立的,並行性就能作到最大化。一旦你在多個線程中共享狀態,就必須以同步方式訪問這些狀態,那樣程序的並行性就變差了。
有多種方式能夠控制並行處理的輸出,能夠把結果存在某些併發集合,或者對結果進行聚合。聚合在並行處理中很常見,Parallel 類的重載方法,也支持這種map/reduce 函數。
下面講任務並行。數據並行重點在處理數據,任務並行則關注執行任務。Parallel 類的Parallel.Invoke 方法能夠執行「分叉/ 聯合」(fork/join)方式的任務並行。調用該方法時,把要並行執行的委託(delegate)做爲傳入參數:
1 void ProcessArray(double[] array) 2 { 3 Parallel.Invoke( 4 () => ProcessPartialArray(array, 0, array.Length / 2), 5 () => ProcessPartialArray(array, array.Length / 2, array.Length) 6 ); 7 } 8 void ProcessPartialArray(double[] array, int begin, int end) 9 { 10 // CPU 密集型的操做…… 11 }
數據並行和任務並行都使用動態調整的分割器,把任務分割後分配給工做線程。線程池在須要的時候會增長線程數量。線程池線程使用工做竊取隊列(work-stealing queue)。微軟公司爲了讓每一個部分儘量高效,作了不少優化。要讓程序獲得最佳的性能,有不少參數能夠調節。只要任務時長不是特別短,採用默認設置就會運行得很好。
若是任務過短,把數據分割進任務和在線程池中調度任務的開銷會很大。若是任務太長,線程池就不能進行有效的動態調整以達到工做量的平衡。很難肯定「過短」和「太長」的判斷標準,這取決於程序所解決問題的類型以及硬件的性能。根據一個通用的準則,只要沒有致使性能問題,我會讓任務儘量短(若是任務過短,程序性能會忽然下降)。更好的作法是使用Parallel 類型或者PLINQ,而不是直接使用任務。這些並行處理的高級形式,自帶有自動分配任務的算法(而且會在運行時自動調整)。
5、多線程編程簡介
線程是一個獨立的運行單元,每一個進程內部有多個線程,每一個線程能夠各自同時執行指令。每一個線程有本身獨立的棧,可是與進程內的其餘線程共享內存。對某些程序來講,其中有一個線程是特殊的,例如用戶界面程序有一個UI 線程,控制檯程序有一個main 線程。
每一個.NET 程序都有一個線程池,線程池維護着必定數量的工做線程,這些線程等待着執行分配下來的任務。線程池能夠隨時監測線程的數量。配置線程池的參數多達幾十個,可是建議採用默認設置,線程池的默認設置是通過仔細調整的,適用於絕大多數現實中的應用場景。