《C#並發編程經典實例》筆記


1.前言

最近趁着項目的一段平穩期研讀了不少書籍,其中《C#並發編程經典實例》給我的印象還是比較深刻的。當然,這可能是由於近段日子看的書大多嘴炮大於實際,如《Head First設計模式》《Cracking the coding interview》等,所以陡然見到一本打着“實例”旗號的書籍,還是挺讓我覺得耳目一新。本着分享和加深理解的目的,我特地整理了一些筆記(主要是Web開發中容易涉及的內容,所以部分章節如數據流,RX等我看了看就直接跳過了),以供審閱學習。語言和技術的魅力,真是不可捉摸

2.開宗明義

一直以來都有一種觀點是實現底層架構,編寫驅動和引擎,或者是框架和工具開發的才是高級開發人員,做上層應用的人僅僅是“碼農”,其實能夠利用好平台提供的相關類庫,而不是全部采用底層技術自己實現,開發出高質量,穩定的應用程序,對技術能力的考驗並不低於開發底層庫,如TPL,async,await等。

3.開發原則和要點

(1)並發編程概述

  1. 並發:同時做多件事情
  2. 多線程:並發的一種形式,它采用多個線程來執行程序
  3. 並行處理:把正在執行的大量的任務分割成小塊,分配給多個同時運行的線程
  4. 並行處理是多線程的一種,而多線程是並發的一種處理形式
  5. 異步編程:並發的一種形式,它采用future模式或者callback機制,以避免產生不必要的線程
  6. 異步編程的核心理念是異步操作:啟動了的操作會在一段時間后完成。這個操作正在執行時,不會阻塞原來的線程。啟動了這個操作的線程,可以繼續執行其他任務。當操作完成后,會通知它的future,或者調用回調函數,以便讓程序知道操作已經結束
  7. await關鍵字的作用:啟動一個將會被執行的Task(該Task將在新線程中運行),並立即返回,所以await所在的函數不會被阻塞。當Task完成后,繼續執行await后面的代碼
  8. 響應式編程:並發的一種基於聲明的編程方式,程序在該模式中對事件作出反應
  9. 不要用 void 作為 async 方法的返回類型! async 方法可以返回 void,但是這僅限於編寫事件處理程序。一個普通的 async 方法如果沒有返回值,要返回 Task,而不是 void
  10. async 方法在開始時以同步方式執行。在 async 方法內部,await 關鍵字對它的參數執行一個異步等待。它首先檢查操作是否已經完成,如果完成了,就繼續運行 (同步方式)。否則,它會暫停 async 方法,並返回,留下一個未完成的 task。一段時間后, 操作完成,async
    方法就恢復運行。
  11. await代碼中拋出異常后,異常會沿着Task方向前進到引用處
  12. 你一旦在代碼中使用了異步,最好一直使用。調用 異步方法時,應該(在調用結束時)用 await 等待它返回的 task 對象。一定要避免使用 Task.Wait 或 Task .Result 方法,因為它們會導致死鎖
  13. 線程是一個獨立的運行單元,每個進程內部有多個線程,每個線程可以各自同時執行指令。 每個線程有自己獨立的棧,但是與進程內的其他線程共享內存
  14. 每個.NET應用程序都維護着一個線程池,這種情況下,應用程序幾乎不需要自行創建新的線程。你若要為 COM interop 程序創建 SAT 線程,就得 創建線程,這是唯一需要線程的情況
  15. 線程是低級別的抽象,線程池是稍微高級一點的抽象
  16. 並發編程用到的集合有兩類:並發變成+不可變集合
  17. 大多數並發編程技術都有一個類似點:它們本質上都是函數式的。這里的函數式是作為一種基於函數組合的編程模式。函數式的一個編程原則是簡潔(避免副作用),另一個是不變性(指一段數據不能被修改)
  18. .NET 4.0 引入了並行任務庫(TPL),完全支持數據並行和任務並行。但是一些資源較少的 平台(例如手機),通常不支持 TPL。TPL 是 .NET 框架自帶的

(2)異步編程基礎

  1. 指數退避是一種重試策略,重試的延遲時間會逐 次增加。在訪問 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);
    }
}
  1. Task.Delay 適合用於對異步代碼進行單元測試或者實現重試邏輯。要實現超時功能的話, 最好使用 CancellationToken
  2. 如何實現一個具有異步簽名的同步方法。如果從異步接口或基類繼承代碼,但希望用同步的方法來實現它,就會出現這種情況。解決辦法是可以使用 Task.FromResult 方法創建並返回一個新的 Task 對象,這個 Task 對象是已經 完成的,並有指定的值
  3. 使用 IProgress 和 Progress 類型。編寫的 async 方法需要有 IProgress 參數,其 中 T 是需要報告的進度類型,可以展示操作的進度
  4. Task.WhenALl可以等待所有任務完成,而當每個Task拋出異常時,可以選擇性捕獲異常
  5. Task.WhenAny可以等待任一任務完成,使用它雖然可以完成超時任務(其中一個Task設為Task.Delay),但是顯然用專門的帶有取消標志的超時函數處理比較好
  6. 第一章提到async和上下文的問題:在默認情況下,一個 async 方法在被 await 調用后恢復運行時,會在原來的上下文中運行。而加上擴展方法ConfigureAwait(false)后,則會在await之后丟棄上下文

(3)並行開發的基礎

  1. 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)
{
    // 計算密集型的處理過程 ...  
}
  1. 在並發編程中,Task類有兩個作用:作為並行任務,或作為異步任務。並行任務可以使用 阻塞的成員函數,例如 Task.Wait、Task.Result、Task.WaitAll 和 Task.WaitAny。並行任務通常也使用 AttachedToParent 來建立任務之間的“父 / 子”關系。並行任務的創建需要 用 Task.Run 或者 Task.Factory.StartNew。
  2. 相反的,異步任務應該避免使用阻塞的成員函數,而應該使用 await、Task.WhenAll 和 Task. WhenAny。異步任務不使用 AttachedToParent,但可以通過 await 另一個任務,建立一種隱 式的“父 / 子”關系。

(4)測試技巧

  1. MSTest從Visual Studio2012 版本開始支持 async Task 類型的單元測試
  2. 如果單元測試框架不支持 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);
        }
    }
  1. 在 async 代碼中,關鍵准則之一就是避免使用 async void。我非常建議大家在對 async void 方法做單元測試時進行代碼重構,而不是使用 AsyncContext。

(5)集合

  1. 線程安全集合是可同時被多個線程修改的可變集合。線程安全集合混合使用了細粒度鎖定和無鎖技術,以確保線程被阻塞的時間最短(通常情況下是根本不阻塞)。對很多線程安全集合進行枚舉操作時,內部創建了該集合的一個快照(snapshot),並對這個快照進行枚舉操作。線程安全集合的主要優點是多個線程可以安全地對其進行訪問,而代碼只會被阻塞很短的時間,或根本不阻塞

  2. ConcurrentDictionary<TKey, TValue>是數據結構中的精品,它是線程安全的,混合使用了細粒度鎖定和無鎖技術,以確保絕大多數情況下能進行快速訪問.

  3. ConcurrentDictionary<TKey, TValue> 內置了AddOrUpdate, TryRemove, TryGetValue等方法。如果多個線程讀寫一個共享集合,使用ConcurrentDictionary<TKey, TValue>是最合適的,如果不會頻繁修改,那就更適合使用ImmutableDictionary<TKey, TValue>。而如果是一些線程只添加元素,一些線程只移除元素,最好使用生產者/消費者集合

(6)函數式OOP

  1. 異步編程是函數式的(functional),.NET 引入的async讓開發者進行異步編程的時候也能用過程式編程的思維來進行思考,但是在內部實現上,異步編程仍然是函數式的

偉人說過,世界既是過程式的,也是函數式的,但是終究是函數式的

  1. 可以用await等待的是一個類(如Task 對象),而不是一個方法。可以用await等待某個方法返回的Task,無論它是不是async方法。

  2. 類的構造函數里是不能進行異步操作的,一般可以使用如下方法。相應的,我們可以通過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();
        }

    }

  1. 在編寫異步事件處理器時,事件參數類最好是線程安全的。要做到這點,最簡單的辦法就 是讓它成為不可變的(即把所有的屬性都設為只讀)

(7)同步

  1. 同步的類型主要有兩種:通信和數據保護

  2. 如果下面三個條件都滿足,就需要用同步來保護共享的數據

  • 多段代碼正在並發運行
  • 這幾段代碼在訪問(讀或寫)同一個數據
  • 至少有一段代碼在修改(寫)數據
  1. 觀察以下代碼,確定其同步和運行狀態
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。

  1. 不可變類型本身就是線程安全的,修改一個不可變集合是不可能的,即便使用多個Task.Run向集合中添加數據,也並不需要同步操作

  2. 線程安全集合(例如 ConcurrentDictionary)就完全不同了。與不可變集合不同,線程安 全集合是可以修改的。線程安全集合本身就包含了所有的同步功能

  3. 關於鎖的使用,有四條重要的准則

  • 限制鎖的作用范圍(例如把lock語句使用的對象設為私有成員)
  • 文檔中寫清鎖的作用內容
  • 鎖范圍內的代碼盡量少(鎖定時不要進行阻塞操作)
  • 在控制鎖的時候絕不運行隨意的代碼(不要在語句中調用事件處理,調用虛擬方法,調用委托)
  1. 如果需要異步鎖,請嘗試 SemaphoreSlim

  2. 不要在 ASP. NET 中使用 Task.Run,這是因為在 ASP.NET 中,處理請求的代碼本來就是在線程池線程中運行的,強行把它放到另一個線程池線程通常會適得其反

(7) 實用技巧

  1. 程序的多個部分共享了一個資源,現在要在第一次訪問該資源時對它初始化
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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM