標籤:nload des 計算 har 版本 htm block res table
- 1.前言
- 2.開宗明義
- 3.開發原則和要點
- (1)並發編程概述
- (2)非同步編程基礎
- (3)並行開發的基礎
- (4)測試技巧
- (5)集合
- (6)函數式OOP
- (7)同步
1.前言
最近趁著項目的一段平穩期研讀了不少書籍,其中《C#並發編程經典執行個體》給我的印象還是比較深刻的。當然,這可能是由於近段日子看的書大多嘴炮大於實際,如《Head First設計模式》《Cracking the coding interview》等,所以陡然見到一本打著“執行個體”旗號的書籍,還是挺讓我覺得耳目一新。本著分享和加深理解的目的,我特地整理了一些筆記(主要是Web開發中容易涉及的內容,所以部分章節如資料流,RX等我看了看就直接跳過了),以供審閱學習。語言和技術的魅力,真是不可捉摸
2.開宗明義
一直以來都有一種觀點是實現底層架構,編寫驅動和引擎,或者是架構和工具開發的才是進階開發人員,做上層應用的人僅僅是“碼農”,其實能夠利用好平台提供的相關類庫,而不是全部採用底層技術自己實現,開發出高品質,穩定的應用程式,對技術能力的考驗並不低於開發底層庫,如TPL,async,await等。
3.開發原則和要點(1)並發編程概述
- 並發:同時做多件事情
- 多線程:並發的一種形式,它採用多個線程來執行程式
- 平行處理:把正在執行的大量的任務分割成小塊,分配給多個同時啟動並執行線程
- 平行處理是多線程的一種,而多線程是並發的一種處理形式
- 非同步編程:並發的一種形式,它採用future模式或者callback機制,以避免產生不必要的線程
- 非同步編程的核心理念是非同步作業:啟動了的操作會在一段時間後完成。這個操作正在執行時,不會阻塞原來的線程。啟動了這個操作的線程,可以繼續執行其他任務。當操作完成後,會通知它的future,或者調用回呼函數,以便讓程式知道操作已經結束
- await關鍵字的作用:啟動一個將會被執行的Task(該Task將在新線程中運行),並立即返回,所以await所在的函數不會被阻塞。當Task完成後,繼續執行await後面的代碼
- 響應式編程:並發的一種基於聲明的編程方式,程式在該模式中對事件作出反應
- 不要用 void 作為 async 方法的傳回型別! async 方法可以返回 void,但是這僅限於編寫事件處理常式。一個普通的 async 方法如果沒有傳回值,要返回 Task,而不是 void
- async 方法在開始時以同步方式執行。在 async 方法內部,await 關鍵字對它的參數執行一個非同步等待。它首先檢查操作是否已經完成,如果完成了,就繼續運行 (同步方式)。否則,它會暫停 async 方法,並返回,留下一個未完成的 task。一段時間後, 操作完成,async 方法就恢複運行。
- await代碼中拋出異常後,異常會沿著Task方向前進到引用處
- 你一旦在代碼中使用了非同步,最好一直使用。調用 非同步方法呼叫時,應該(在調用結束時)用 await 等待它返回的 task 對象。一定要避免使用 Task.Wait 或 Task.Result 方法,因為它們會導致死結
- 線程是一個獨立的運行單元,每個進程內部有多個線程,每個線程可以各自同時執行指令。 每個線程有自己獨立的棧,但是與進程內的其他線程共用記憶體
- 每個.NET應用程式都維護著一個線程池,這種情況下,應用程式幾乎不需要自行建立新的線程。你若要為 COM interop 程式建立 SAT 線程,就得 建立線程,這是唯一需要線程的情況
- 線程是低層級的抽象,線程池是稍微進階一點的抽象
- 並發編程用到的集合有兩類:並發變成+不可變集合
- 大多數並發編程技術都有一個類似點:它們本質上都是函數式的。這裡的函數式是作為一種基於函數組合的編程模式。函數式的一個編程原則是簡潔(避免副作用),另一個是不變性(指一段資料不能被修改)
- .NET 4.0 引入了並行任務庫(TPL),完全支援資料並行和任務並行。但是一些資源較少的 平台(例如手機),通常不支援 TPL。TPL 是 .NET 架構內建的
(2)非同步編程基礎
- 指數退避是一種重試策略,重試的延遲時間會逐 次增加。在訪問 Web 服務時,最好的方式就是採用指數退避,它可以防止伺服器被太多的重試阻塞
static async Task<string> DownloadStringWithRetries(string uri){ using (var client = new HttpClient()) { // 第 1 次重試前等 1 秒,第 2 次等 2 秒,第 3 次等 4 秒。 var nextDelay = TimeSpan.FromSeconds(1); for (int i = 0; i != 3; ++i) { try { return await client.GetStringAsync(uri); } catch { } await Task.Delay(nextDelay); nextDelay = nextDelay + nextDelay; } // 最後重試一次,以便讓調用者知道出錯資訊。 return await client.GetStringAsync(uri); }}
- Task.Delay 適合用於對非同步代碼進行單元測試或者實現重試邏輯。要實現逾時功能的話, 最好使用 CancellationToken
- 如何?一個具有非同步簽名的同步方法。如果從非同步介面或基類繼承代碼,但希望用同步的方法來實現它,就會出現這種情況。解決辦法是可以使用 Task.FromResult 方法建立並返回一個新的 Task 對象,這個 Task 對象是已經 完成的,並有指定的值
- 使用 IProgress 和 Progress 類型。編寫的 async 方法需要有 IProgress 參數,其 中 T 是需要報告的進度類型,可以展示操作的進度
- Task.WhenALl可以等待所有任務完成,而當每個Task拋出異常時,可以選擇性捕獲異常
- Task.WhenAny可以等待任一任務完成,使用它雖然可以完成逾時任務(其中一個Task設為Task.Delay),但是顯然用專門的帶有取消標誌的逾時函數處理比較好
- 第一章提到async和內容相關的問題:在預設情況下,一個 async 方法在被 await 調用後恢複運行時,會在原來的上下文中運行。而加上擴充方法ConfigureAwait(false)後,則會在await之後丟棄上下文
(3)並行開發的基礎
- Parallel 類有一個簡單的成員 Invoke,可用於需要並行調用一批方法,並且這些方法(大部分)是互相獨立的
static void ProcessArray(double[] array){ Parallel.Invoke( () => ProcessPartialArray(array, 0, array.Length / 2), () => ProcessPartialArray(array, array.Length / 2,array.Length));}static void ProcessPartialArray(double[] array, int begin, int end){ // 計算密集型的處理過程 ... }
- 在並發編程中,Task類有兩個作用:作為並行任務,或作為非同步任務。並行任務可以使用 阻塞的成員函數,例如 Task.Wait、Task.Result、Task.WaitAll 和 Task.WaitAny。並行任務通常也使用 AttachedToParent 來建立任務之間的“父 / 子”關係。並行任務的建立需要 用 Task.Run 或者 Task.Factory.StartNew。
- 相反的,非同步任務應該避免使用阻塞的成員函數,而應該使用 await、Task.WhenAll 和 Task. WhenAny。非同步任務不使用 AttachedToParent,但可以通過 await 另一個任務,建立一種隱 式的“父 / 子”關係。
(4)測試技巧
- MSTest從Visual Studio2012 版本開始支援 async Task 類型的單元測試
- 如果單元測試架構不支援 async Task 類型的單元測試,就需要做一些額外的修改才能等待非同步作業。其中一種做法是使用 Task.Wait,並在有錯誤時拆開 AggregateException 對象。我的建議是使用 NuGet 包 Nito.AsyncEx 中的 AsyncContext 類
這裡附上一個ABP中實現的可操作AsyncHelper類,就是基於AsyncContext實現
/// <summary> /// Provides some helper methods to work with async methods. /// </summary> public static class AsyncHelper { /// <summary> /// Checks if given method is an async method. /// </summary> /// <param name="method">A method to check</param> public static bool IsAsyncMethod(MethodInfo method) { return ( method.ReturnType == typeof(Task) || (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) ); } /// <summary> /// Runs a async method synchronously. /// </summary> /// <param name="func">A function that returns a result</param> /// <typeparam name="TResult">Result type</typeparam> /// <returns>Result of the async operation</returns> public static TResult RunSync<TResult>(Func<Task<TResult>> func) { return AsyncContext.Run(func); } /// <summary> /// Runs a async method synchronously. /// </summary> /// <param name="action">An async action</param> public static void RunSync(Func<Task> action) { AsyncContext.Run(action); } }
- 在 async 代碼中,關鍵準則之一就是避免使用 async void。我非常建議大家在對 async void 方法做單元測試時進行代碼重構,而不是使用 AsyncContext。
(5)集合
安全執行緒集合是可同時被多個線程修改的可變集合。安全執行緒集合混合使用了細部鎖定定和無鎖技術,以確保線程被阻塞的時間最短(通常情況下是根本不阻塞)。對很多安全執行緒集合進行枚舉操作時,內部建立了該集合的一個快照(snapshot),並對這個快照進行枚舉操作。安全執行緒集合的主要優點是多個線程可以安全地對其進行訪問,而代碼只會被阻塞很短的時間,或根本不阻塞
ConcurrentDictionary<tkey, tvalue="">是資料結構中的精品,它是安全執行緒的,混合使用了細部鎖定定和無鎖技術,以確保絕大多數情況下能進行快速存取.
ConcurrentDictionary<tkey, tvalue=""> 內建了AddOrUpdate, TryRemove, TryGetValue等方法。如果多個線程讀寫一個共用集合,使用ConcurrentDictionary<tkey, tvalue="">是最合適的,如果不會頻繁修改,那就更適合使用ImmutableDictionary<tkey, tvalue="">。而如果是一些線程只添加元素,一些線程只移除元素,最好使用生產者/消費者集合
(6)函數式OOP
非同步編程是函數式的(functional),.NET 引入的async讓開發人員進行非同步編程的時候也能用過程式編程的思維來進行思考,但是在內部實現上,非同步編程仍然是函數式的
偉人說過,世界既是過程式的,也是函數式的,但是終究是函數式的
可以用await等待的是一個類(如Task對象),而不是一個方法。可以用await等待某個方法返回的Task,無論它是不是async方法。
類的建構函式裡是不能進行非同步作業的,一般可以使用如下方法。相應的,我們可以通過var instance=new Program.CreateAsync();
class Program { private Program() { } private async Task<Program> InitializeAsync() { await Task.Delay(TimeSpan.FromSeconds(1)); return this; } public static Task<Program> CreateAsync() { var result = new Program(); return result.InitializeAsync(); } }
- 在編寫非同步事件處理器時,事件參數類最好是安全執行緒的。要做到這點,最簡單的辦法就 是讓它成為不可變的(即把所有的屬性都設為唯讀)
(7)同步
同步的類型主要有兩種:通訊和資料保護
- 如果下面三個條件都滿足,就需要用同步來保護共用的資料
- 多段代碼正在並發運行
- 這幾段代碼在訪問(讀或寫)同一個資料
- 至少有一段代碼在修改(寫)資料
- 觀察以下代碼,確定其同步和運行狀態
class SharedData{ public int Value { get; set; }}async Task ModifyValueAsync(SharedData data){ await Task.Delay(TimeSpan.FromSeconds(1)); data.Value = data.Value + 1;}// 警告:可能需要同步,見下面的討論。async Task<int> ModifyValueConcurrentlyAsync(){ var data = new SharedData(); // 啟動三個並發的修改過程。 var task1 = ModifyValueAsync(data); var task2 = ModifyValueAsync(data); var task3 = ModifyValueAsync(data); await Task.WhenAll(task1, task2, task3); return data.Value;}
本例中,啟動了三個並發啟動並執行修改過程。需要同步嗎?答案是“看情況”。如果能確定 這個方法是在 GUI 或 ASP.NET 上下文中調用的(或同一時間內只允許一段代碼啟動並執行任 何其他上下文),那就不需要同步,因為這三個修改資料過程的已耗用時間是互不相同的。 例如,如果它在 GUI 上下文中運行,就只有一個 UI 線程可以運行這些資料修改過程,因 此一段時間內只能運行一個過程。因此,如果能夠確定是“同一時間只運行一段代碼”的 上下文,那就不需要同步。但是如果從線程池線程(如 Task.Run)調用這個方法,就需要同步了。在那種情況下,這三個資料修改過程會在獨立的線程池線程中運行,並且同時修改 data.Value,因此必須同步地訪問 data.Value。
不可變類型本身就是安全執行緒的,修改一個不可變集合是不可能的,即便使用多個Task.Run向集合中添加資料,也並不需要同步操作
安全執行緒集合(例如 ConcurrentDictionary)就完全不同了。與不可變集合不同,線程安 全集合是可以修改的。安全執行緒集合本身就包含了所有的同步功能
- 關於鎖的使用,有四條重要的準則
- 限制鎖的作用範圍(例如把lock語句使用的對象設為私人成員)
- 文檔中寫清鎖的作用內容
- 鎖範圍內的代碼盡量少(鎖定時不要進行阻塞操作)
- 在控制鎖的時候絕不運行隨意的代碼(不要在語句中呼叫事件處理,調用虛擬方法,調用委託)
如果需要非同步鎖,請嘗試 SemaphoreSlim
不要在 ASP. NET 中使用 Task.Run,這是因為在 ASP.NET 中,處理請求的代碼本來就是線上程池線程中啟動並執行,強行把它放到另一個線程池線程通常會適得其反
(7) 實用技巧
- 程式的多個部分共用了一個資源,現在要在第一次訪問該資源時對它初始化
static int _simpleValue;static readonly Lazy<Task<int>> MySharedAsyncInteger = new Lazy<Task<int>>(() => Task.Run(async () => { await Task.Delay(TimeSpan.FromSeconds(2)); return _simpleValue++; }));async Task GetSharedIntegerAsync(){ int sharedValue = await MySharedAsyncInteger.Value;}
《C#並發編程經典執行個體》筆記