2019年11月26日 星期二

.NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 4 在多執行緒下,使用 Interlocked 來做到執行緒安全

.NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 4 在多執行緒下,使用 Interlocked 來做到執行緒

  1. .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 1 在單一執行緒下,同步執行加一與減一方法
  2. .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 2 在多執行緒下,非同步執行加一與減一方法,造成執行緒不安全的現象
  3. .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 3 在多執行緒下,使用 lock 關鍵字來做到執行緒安全
  4. .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 4 在多執行緒下,使用 Interlocked 來做到執行緒安全
  5. .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 5 在多執行緒下,不要全部都使用執行緒同步機制 來做到執行緒安全

    從上一篇文章中,.NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 3 在多執行緒下,使用 lock 關鍵字來做到執行緒安全 中,透過使用 C# 關鍵字來設計出一個執行緒安全的程式碼,可是,這樣的設計工作只做到一半,因為整體執行效能真的太差了;在這裡使用到的 lock 關鍵字是屬於作業系統的核心模式 Kernel Model 下的執行緒同步機制,通常我們撰寫的程式都會在使用者模式 User Mode 下來執行,若要在使用者模式下來呼叫核心模式的 API,是會造成效能上的傷害。
    將把加一與減一這兩個方法,同時在不同的執行緒使用非同步的方式來執行,不過,卻發現到只要當時的系統環境有多可用邏輯處理器可以使用,就會得到不正確的結果與執行速度比起同步執行方法來的慢了些。在設計任何多執行緒程式碼的時候,首要工作就是要能夠確保所設計的程式碼是具備有執行緒安全的效果,也就是對於這裡所提出的範例中,每次執行的結果必須對於 AddSub.counter 這個共用靜態變數於最後執行結果必須為0。
    因此,在這篇文章中,將會嘗試使用 .NET 提供的使用者模式的執行緒同步機制,在這裡會使用 Interlocked 這個類別所提供的靜態方法來做到,該類別將會為多重執行緒共用的變數提供不可部分完成的作業 (Atomic Operation),現在來看看這樣的做法是否還是會達成執行緒安全的特性,並且是否可以達成提升執行速度的目的。

    使用 Interlocked

    因為對於 counter++ 與 counter-- 這兩個運算式,是屬於 可部分完成的作業 (也就是非 Atomic Operation),因此,在這裡將會使用 Interlocked 所提供的靜態方法來做到同樣的需求。
    C Sharp / C#
    class AddSub
    {
        public static int counter = 0;
        public static object locker = new object();
        public void Adds(AddSubAction addSubAction = AddSubAction.NoLock)
        {
            for (int i = 0; i < int.MaxValue; i++)
            {
                    lock (locker)
                    {
                        Interlocked.Add(ref counter, 1);
                    }
            }
        }
        public void Subs(AddSubAction addSubAction = AddSubAction.NoLock)
        {
            for (int i = 0; i < int.MaxValue; i++)
            {
                    lock (locker)
                    {
                        Interlocked.Add(ref counter, -1);
                    }
            }
        }
    }
    
    在上面的程式碼中,將會把原先的 counter++ 運算式變更成為呼叫這個方法 Interlocked.Add(ref counter, 1);,如此,對於要每次加一的計算程式碼,就不會受到多執行緒運作的競賽條件 Race Condition 所造成的影響;而對於原先要減一的運算,也就 counter-- 將會使用 Interlocked.Add(ref counter, -1); 這個使用者模式的執行緒同步建構子來做到。
    底下將會使用 C# lock 關鍵字,並請指定不同邏輯處理器數量下的執行結果。
    ThreadSynchronization yes UserModeLock 10000000
    Counter=0, 27,920ms
    
    ThreadSynchronization yes UserModeLock 10100000
    Counter=0, 77,253ms
    
    ThreadSynchronization yes UserModeLock 10101000
    Counter=0, 72,509ms
    
    ThreadSynchronization yes UserModeLock 11000000
    Counter=0, 25,082ms
    
    ThreadSynchronization yes UserModeLock 11100000
    Counter=0, 42,099ms
    
    ThreadSynchronization yes UserModeLock 11110000
    Counter=0, 39,930ms
    
    ThreadSynchronization yes UserModeLock 11111111
    Counter=0, 40,941ms
    
    從上面的執行結果可以看出,不論指定的邏輯處理器數量是多少,當多執行緒程式執行完畢之後,這個程式的執行結果是正確的;另外,對於整體執行所花費的時間會因為指定的邏輯處理器數量或者稱是否同時有使用到同一個核心的兩個邏輯處理,而有所不同,不過,執行速度大約介於 40秒~77秒之間,甚感欣慰一點則是,這樣的執行效能比起使用 C# 的 lock 關鍵字來說,已經好了太多了,這是因為這裡並沒有使用到作業系統核心模式 API,而是使用到使用者模式下的同步建構子 API。
    對於這樣的表現,其實,還是與同步程式碼有些落差,在最後一篇文章中,將會提出一個徹底的解決方案,可以做到執行緒安全並且提升比同步程式執行速度還要快的作法。
    總之,現在已經設計出一個具有執行緒安全的程式碼了,接下來要來嘗試解決執行速度過慢的問題。



    沒有留言:

    張貼留言