2019年11月29日 星期五

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

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

  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 4 在多執行緒下,使用 Interlocked 來做到執行緒安全 中,雖然將核心模式的執行緒同步機制,變更成為使用者模式的同步機制,得到的是執行緒安全的執行結果,不過,執行效能卻還是明顯的很大的落差。
在進行多執行緒非同步程式設計的時候,若多個執行緒要同時存取同一個共用物件的時候,會因為有競賽條件 Race Condition 的問題,而造成執行結果不正確的問題,這個時候需要導入執行緒同步機制來解決執行結果不確定或者不正確的問題;可是,不論導入的是核心模式或者使用者模式的執行緒同步機制,都會造成執行效能的影響;真造想要提升執行速度,唯一個解決方案就是不要使用執行緒同步機制,這樣就可以提升執行速度,而解決的其中一個方式就是透過了該執行緒委派方法內的區域變數 Local Variable 來解決此一問題,在這裡因為該區域變數存在於每個執行緒內的堆疊記憶體中,當要進行存取這些區域變數物件的時候,是不會有執行緒競賽條件的問題,每個執行緒都會存取自己本身的變數;在這個應用範例中,可以設計當整個迴圈都執行完畢之後,再把儲存在區域變數的執行結果,使用核心模式同步機制API或者使用者模式同步機制API,將結果更新到共享靜態變數中,這樣,就會降低整體使用執行緒同步機制的次數,當然,也會提升整體執行速度囉。
現在,來看看要如何這樣處理。

減少使用同步機制的次數

因為對於 counter++ 與 counter-- 這兩個運算式,是屬於 可部分完成的作業 (也就是非 Atomic Operation),因此,在這裡將會使用 Interlocked 所提供的靜態方法來做到同樣的需求。
C Sharp / C#
class AddSub
{
    public static int counter = 0;
    public void Adds(AddSubAction addSubAction = AddSubAction.NoLock)
    {
        int localCounter = 0;
        for (int i = 0; i < int.MaxValue; i++)
        {
                localCounter++;
        }
            Interlocked.Add(ref counter, localCounter);
    }
    public void Subs(AddSubAction addSubAction = AddSubAction.NoLock)
    {
        int localCounter = 0;
        for (int i = 0; i < int.MaxValue; i++)
        {
                localCounter--;
        }
            Interlocked.Add(ref counter, localCounter);
    }
}
在上面的程式碼中,將會把原先的 counter++ 運算式變更成為呼叫 localCounter++,其中, localCounter 這個變數是定義在這個方法內,並不會與其他的執行緒共享,每個執行緒僅能夠存去到自己本身的 localCounter 變數,因此,在這個大量次數的迴圈內,進行 localCounter++ 或者 localCounter-- 計算,並不會造成有執行緒競爭條件的問題產生。
當迴圈執行完畢之後,會將 localCounter 變數的值,是設定到共享靜態變數 counter 內,此時,將會透使用者模式提供的執行緒同步機制 Interlocked,將區域變數的數值,加總到共享靜態變數上,如此,在加一或者減一的方法內,就會存在著一次執行緒同步的呼叫。
現在,來看看指定不同邏輯處理器數量下的執行結果。
ThreadSynchronization yes NoLockByLocal 10000000
Counter=0, 4,923ms

ThreadSynchronization yes NoLockByLocal 10100000
Counter=0, 2,482ms

ThreadSynchronization yes NoLockByLocal 10101000
Counter=0, 2,593ms

ThreadSynchronization yes NoLockByLocal 11000000
Counter=0, 4,835ms

ThreadSynchronization yes NoLockByLocal 11100000
Counter=0, 3,102ms

ThreadSynchronization yes NoLockByLocal 11110000
Counter=0, 3,339ms

ThreadSynchronization yes NoLockByLocal 11111111
Counter=0, 3,148ms
從上面的執行結果可以看出,不論執行效能與執行結果的正確性,都是令人相當滿意的。
+

執行結果是正確的,Counter 這個共享靜態變數,經過多執行緒的加一與減一非同步計算的結果,得到的是 0,這是符合預期的;而且,不論執行幾次,使用多少數量的邏輯處理器,執行結果都是相同的,而且若是同時使用更多的執行緒來執行這兩個加一與減一的方法,執行結果也都是 Counter=0。
對於執行效能上,表現得也是相當的亮眼,除了 10000000 與 11000000 這兩種邏輯處理器數量模式,需要花費大約 5 秒的時間 (這個時間已經比起同步執行程式碼的 7.5 秒,提升了不少執行效能),對於採用其他邏輯處理器數量的模式下,大約可以維持 2.5秒 ~ 3.3 秒之間的處理速度;對於 10000000 與 11000000 這兩種邏輯處理器數量模式,其實都是指定在同一個 CPU Core 上來執行,因為這台電腦的 CPU 具備了 Hyper-Threading 技術,同一個 CPU Core 可以具備有兩個 邏輯處理器效果,但是,這並不代表這個 CPU Core 的可以處理效能具備有兩倍能力,畢竟 Hyper-Threading 技術僅能夠提升執行速度約 20~30%;而當指定了不同 CPU Core 的時候,就可以看到多執行緒程式的執行速度明顯的提升相當的多。



沒有留言:

張貼留言