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 的時候,就可以看到多執行緒程式的執行速度明顯的提升相當的多。



2019年11月27日 星期三

在 ASP.NET Core 3.0 上使用 Unity IoC / DI 容器 Container

在 ASP.NET Core 3.0 上使用 Unity IoC / DI 容器 Container

為了要能夠在 ASP.NET Core 3.0 的專案內使用 Unity DI 容器功能,可以參考底下的步驟來實作出來
首先,請先建立一個空白的 ASP.NET Core 3.0 的專案
您需要安裝這個 Unity.Microsoft.DependencyInjection 套件 (在撰寫這篇文章的時候,最新的版本將是 5.10.2),其舊的 Github 專案位於 aspnet/DependencyInjection 現在已經轉移到了 aspnet/Extensions,其中關於相依性注入的原始碼位於 Extensions/src/DependencyInjection/
先打開 [Program.cs] 檔案,底下是建立專案的時候所產生的預設程式碼
C Sharp / C#
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
為了要能夠在 ASP.NET Core 3.0 的專案內,使用 Unity IoC 容器作為相依性注入管理之用,請將 [Program.cs] 檔案的內容,修改成為底下程式碼
C Sharp / C#
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory<IUnityContainer>(new UnityContainerFactory())
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
partial class UnityContainerFactory : IServiceProviderFactory<IUnityContainer>
{
    private IServiceCollection Services { get; set; }

    public IUnityContainer CreateBuilder(IServiceCollection services)
    {
        Services = services;
        return new UnityContainer();
    }

    public IServiceProvider CreateServiceProvider(IUnityContainer containerBuilder)
    => containerBuilder.BuildServiceProvider(Services);
}
在上面的程式碼中,會先要建立一個新的類別 [UnityContainerFactory] 該類別需要實作 [IServiceProviderFactory] 介面,由於在這裡將會要導入 Unity 容器,因此,將會實作 [IServiceProviderFactory] 這個泛型介面。
另外,在原先的 CreateHostBuilder 方法內,請加入 .UseServiceProviderFactory<IUnityContainer>(new UnityContainerFactory() 方法呼叫,以啟用 Unity 容器功能。
現在要轉移到 [Startup.cs] 這個檔案內,底下為建立專案後所產生的原始碼。
C Sharp / C#
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}
接下來要修改 [Startup.cs] 檔案,使用 Unity 相依性注入容器的註冊與解析工作,請將 [Startup.cs] 檔案內容,修改成為底下的程式碼。
C Sharp / C#
public interface IMessage
{
    string Send(string message);
}

public class ConsoleMessage : IMessage
{
    public string Send(string message)
    {
        string result = $"ConsoleMessage :{message}";
        Console.WriteLine(result);
        return result;
    }
}
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void ConfigureContainer(IUnityContainer container)
    {
        container.RegisterType<IMessage, ConsoleMessage>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IMessage message)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync(message.Send("Hello World!"));
            });
        });
    }
}
在這個檔案內的最上方,會先建立一個介面 IMessage,與實作該介面的具體實作類別 ConsoleMessage
接著,請在 Startup 類別內,建立一個 public void ConfigureContainer(IUnityContainer container) 方法,由於該方法會傳入一個 IUnityContainer,所以,便可以在這裡進行所需要注入對應型別的註冊宣告,這裡將會使用 container.RegisterType<IMessage, ConsoleMessage>(); 這個敘述,建立起 IMessage 介面與 ConsoleMessage 類別的對應關係。
現在要來測試,是否可以在 ASP.NET Core 3.0 專案內來使用 Unity 容器註冊的型別,直接注入實作執行個體來直接使用,在這裡將會修改 Configure 方法,修改成為 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IMessage message) 其中,在該方法參數的最後面,加入 IMessage message 參數,期望相依性管理容器能夠注入執行個體到這個方法內,如此,便可以在 await context.Response.WriteAsync(message.Send("Hello World!")); 敘述內呼叫 IMessage 具體實作的執行個體方法了。



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。
    對於這樣的表現,其實,還是與同步程式碼有些落差,在最後一篇文章中,將會提出一個徹底的解決方案,可以做到執行緒安全並且提升比同步程式執行速度還要快的作法。
    總之,現在已經設計出一個具有執行緒安全的程式碼了,接下來要來嘗試解決執行速度過慢的問題。



    2019年11月25日 星期一

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

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

    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 2 在多執行緒下,非同步執行加一與減一方法,造成執行緒不安全的現象 中,將把加一與減一這兩個方法,同時在不同的執行緒使用非同步的方式來執行,不過,卻發現到只要當時的系統環境有多可用邏輯處理器可以使用,就會得到不正確的結果與執行速度比起同步執行方法來的慢了些。在設計任何多執行緒程式碼的時候,首要工作就是要能夠確保所設計的程式碼是具備有執行緒安全的效果,也就是對於這裡所提出的範例中,每次執行的結果必須對於 AddSub.counter 這個共用靜態變數於最後執行結果必須為0。
      在這篇文章中,將會使用 C# lock 關鍵字,把需要存取共用變數的關鍵區域 Critical Section 程式碼進行鎖定,讓這個多執行緒的程式具有執行緒安全的特性。
      在這篇文章所提到的專案原始碼,可以從 GitHub 下載

      進行多執行緒的測試,使用 lock 關鍵字

      為了要設計出執行緒安全的程式碼,對於要對共用靜態變數 AddSub.counter 進行加一與減一計算的時候,將使用 lock 關鍵字將其包裝起來,如同底下程式碼
      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)
                      {
                          counter++;
                      }
              }
          }
          public void Subs(AddSubAction addSubAction = AddSubAction.NoLock)
          {
              for (int i = 0; i < int.MaxValue; i++)
              {
                      lock (locker)
                      {
                          counter--;
                      }
              }
          }
      }
      
      經過這樣的修正,當執行緒執行到 counter++ 或者 counter-- 程式碼的時候,將會有 lock 進行鎖定保護,也就是說,這裡使用的 核心模式 Kernel Mode 的同步建構子 Synchronization Constructor 來進行鎖定,透過 lock 關鍵字,可以確保同一個時間,對於 lock 內的程式碼僅會有一個執行緒可以執行 lock 內的程式碼。
      對於使用 lock 關鍵字的時候,需要提供一個物件,這裡將會宣告一個靜態變數 public static object locker = new object();
      底下將會使用 C# lock 關鍵字,並請指定不同邏輯處理器數量下的執行結果。
      ThreadSynchronization yes UsingNETLock 10000000
      Counter=0, 88,960ms
      
      ThreadSynchronization yes UsingNETLock 10100000
      Counter=0, 181,327ms
      
      ThreadSynchronization yes UsingNETLock 10101000
      Counter=0, 133,869ms
      
      ThreadSynchronization yes UsingNETLock 11000000
      Counter=0, 106,076ms
      
      ThreadSynchronization yes UsingNETLock 11100000
      Counter=0, 107,196ms
      
      ThreadSynchronization yes UsingNETLock 11110000
      Counter=0, 113,402ms
      
      ThreadSynchronization yes UsingNETLock 11111111
      Counter=0, 121,130ms
      
      從上面的執行結果可以看到,不論指定的邏輯處理器數量是多少,當多執行緒程式執行完畢之後,這個程式的執行結果是正確的;不過,執行這個多執行緒程式所花費的時間真的慘不忍睹,最好的執行時間竟然是僅使用一顆 CPU 的 10000000 的執行結果,需要花費 89 秒的時間,這比起同步程式執行所花費的時間 7.5 秒左右,真的高出太多了,而且,當使用全部八顆邏輯處理器的時候,整體執行完成的時間卻需要高達 121 秒。
      總之,現在已經設計出一個具有執行緒安全的程式碼了,接下來要來嘗試解決執行速度過慢的問題。



      2019年11月24日 星期日

      .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 2 在多執行緒下,非同步執行加一與減一方法,造成執行緒不安全的現象

      .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 2 在多執行緒下,非同步執行加一與減一方法,造成執行緒不安全的現象

      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 1 在單一執行緒下,同步執行加一與減一方法 中,將會看到若使用同步程式設計方式所建置出來的程式,設計上相單的簡單,而且不容易出錯,可以,卻無法充分的善用電腦上的處理器數量,達到並行處理的效果,也就是可以提升整體應用程式的執行下能;在加一與減一的程式碼中,大約需要花費 7.5 秒的時間才能夠完成。
        那麼,要怎麼才能夠設計出可以具備平行處理的程式碼,充分善用這些處理器來同時執行所設計的程式碼呢?在 .NET / C# 程式語言中可以透過 Thread 這個類別,建立起額外的執行緒,讓這些執行緒可以具備並行執行能力。
        對於要使用執行緒類別可以說相當的容易,建立一個 Thread 執行個體,在建構函式內指派一個委派方法,接著呼叫該 Thread 執行緒執行個體的 Start() 方法就可以做到;可是,若沒有真正理解到執行緒背後的技術原理與可能造成的問題,將會帶來所設計的程式碼不是很容易偵錯,因為,在多執行緒執行環境下,想要知道多執行緒程式碼所造成的問題,是無法很容易地在設計階段輕鬆地看出問題端倪,必須要等到程式執行的時候,才能夠看到問題發生;另外就是,每增加一個執行緒,將會需要額外的成本花費,例如:建置一個執行緒需要額外的記憶體與建置時間、過多的執行緒存在於系統上的時候,將會有許多 CPU 處理時間花費在 內容交換 Context Switch 上,也就是說,在設計多執行緒程式碼的時候,不是一昧的產生出很多的執行緒就可以提稱整體執行效能,畢竟,這台電腦上有幾個處理器核心,同一個時間就僅能夠執行這些處理器數量的執行緒程式碼,其他過多的執行緒就僅能夠在排程器 Scheduler 的佇列 Queue 中進行等待,所以,對於多執行緒的程式設計,需要有較多的關注與設計經驗。
        在此文章中,將會把加一與減一這兩個方法,設計在不同的執行緒下來執行,而且每次迴圈的執行結果,還是會更新到共用靜態變數上;最後,將來看看這樣的設計方式是否可以達到正確的執行結果與提升執行效能的好處。
        在這篇文章所提到的專案原始碼,可以從 GitHub 下載

        進行多執行緒的測試,但是不具備執行緒安全特性

        在這裡將會設計一個新的方法, AsyncAddSub , 在這個方法內將會建立兩個執行緒,底下為這個新方法的程式碼列表。
        C Sharp / C#
        static void AsyncAddSub(AddSubAction addSubAction)
        {
             WaitHandle[] waitHandles = new WaitHandle[]
            {
                new AutoResetEvent(false),
                new AutoResetEvent(false)
            };
           AddSub addSub = new AddSub();
            Thread thread1 = new Thread(x =>
            {
                addSub.Adds(addSubAction);
                (waitHandles[0] as AutoResetEvent).Set();
            });
            Thread thread2 = new Thread(x =>
            {
                addSub.Subs(addSubAction);
                (waitHandles[1] as AutoResetEvent).Set();
            });
        
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            thread1.Start();
            thread2.Start();
        
            WaitHandle.WaitAll(waitHandles);
            stopwatch.Stop();
            Console.WriteLine($"Counter={AddSub.counter}, {stopwatch.ElapsedMilliseconds:N0}ms");
        }
        
        在 AsyncAddSub 方法內,將會使用使用 Thread thread1 = new Thread(x => {...}); 這樣的敘述建立起兩個執行緒,其中這兩個執行緒要執行的委派方法將會使用 Lambda 匿名委派方法來宣告,對於 thread1 這個執行緒,將會呼叫 AddSub 類別所建立的執行個體內的 Adds 方法,而對於 thread2 這個執行緒,將會呼叫 AddSub 類別所建立的執行個體內的 Subs 方法。
        接著,將會使用 thread1.Start(); & thread2.Start(); 這兩個敘述,分別來啟動這兩個執行。傳統上,要知道執行緒是否執行完成,可以執行 thread1.Join(); 或者 thread2.Join(); 方法,不過,當執行 Thread.Join 方法的時候,將會造成當前執行緒進入 Block 封鎖階段,直到該執行緒執行完成之後,才會繼續執行底下的程式碼;然而,在多執行緒執行環境下,開發者是無法確認哪一個執行緒一定會先執行完畢,而就使用該執行緒的 Join 方法來等待,就算先啟動執行的執行緒,也並不代表這個執行緒會先執行完畢。
        現在產生一個問題,要如何知道這兩個執行緒都已經完成了,這裡將會引進了執行緒同步 Synchronizaiton Constructor ,這裡將會使用執行緒同步的 WaitHandl 陣列來儲存兩個 AutoResetEvent 物件,而對於預設的 AutoResetEvent 物件將會預設設定其為 false,表示初始狀態設定為未收到信號。
        當加一或者減一執行緒執行完畢之後,將會呼叫 AutoResetEvent.Set() 方法,將事件的狀態設定為收到信號,在這裡將會表示該執行緒已經執行完成的訊號通知;而在啟動這兩個執行緒之後的程式碼,將會使用 WaitHandle.WaitAll(waitHandles); 敘述來等到這兩個執行緒都已經全部完成,如此,就會接下來執行底下的程式碼。
        如同上面的程式碼,在此設計了一個類別 AddSub ,該類別提供了兩個執行個體方法與一個靜態變數,第一個物件方法 Adds 會執行一個迴圈,將會跑 int.MaxValue 次,每次回圈內將會執行 counter++ 這個表示式,這裡的 counter 是一個靜態變數,但是 counter 的數值異動卻是由執行個體方法來去變更的;第二個物件方法 Subs 會執行一個迴圈,將會跑 int.MaxValue 次,每次回圈內將會執行 counter-- 這個表示式。也就是說 Adds() 這個方法將會再回圈內每次都進行加一的動作,而對於 Subs() 這個方法將會在回圈內每次都進行減一的動作。
        現在,已經完成了兩個執行緒的程式碼設計,而且可以計算這兩個執行緒都全部執行完畢之後所需要的時間,這裡就可以使用這個敘述 ThreadSynchronization yes NoLock 10000000 ,在第一個引數中使用 yes 文字,指定測試程式要使用多執行緒方式來執行,並且不要使用任何的執行緒同步建構子 Synchronizaiton Constructor 來保護要存取共用的靜態變數程式碼。
        底下將會指定在多執行緒下,非同步執行加一與減一方法,並且指定在不同的邏輯處理器數量下的執行結果。
        ThreadSynchronization yes NoLock 10000000
        Counter=0, 7,417ms
        
        ThreadSynchronization yes NoLock 10100000
        Counter=878337449, 7,105ms
        
        ThreadSynchronization yes NoLock 10101000
        Counter=66089567, 14,481ms
        
        ThreadSynchronization yes NoLock 11000000
        Counter=896913837, 6,765ms
        
        ThreadSynchronization yes NoLock 11100000
        Counter=-13693011, 9,710ms
        
        ThreadSynchronization yes NoLock 11110000
        Counter=-6874487, 9,235ms
        
        ThreadSynchronization yes NoLock 11111111
        Counter=-66814967, 9,337ms
        

        發現問題1 : 僅有一個結果是正確的

        從上面的執行結果可以看滿詭異的現象,除了第一個測試 ThreadSynchronization yes NoLock 10000000 所顯示的 Counter 靜態變數數值為 0,其他的都不是為 0,這也就表示了僅有第一個測試方法 ThreadSynchronization yes NoLock 10000000 可以得到正確且預期的執行結果,這裡指定僅使用單一一個邏輯處理器來進行這一的程式碼執行;而對於指定使用多個邏輯處理器的執行結果,都是不正確的。
        ThreadSynchronization yes NoLock 10000000
        Counter=0, 7,456ms
        
        ThreadSynchronization yes NoLock 00100000
        Counter=0, 7,478ms
        
        ThreadSynchronization yes NoLock 00001000
        Counter=0, 7,525ms
        
        ThreadSynchronization yes NoLock 00000010
        Counter=0, 7,592ms
        
        ThreadSynchronization yes NoLock 01000000
        Counter=0, 7,608ms
        
        ThreadSynchronization yes NoLock 00010000
        Counter=0, 7,462ms
        
        ThreadSynchronization yes NoLock 00000100
        Counter=0, 7,521ms
        
        ThreadSynchronization yes NoLock 00000001
        Counter=0, 7,781ms
        
        在這裡,將會重複執行 ThreadSynchronization yes NoLock 10000000 這樣的測試,並且只會指定一個邏輯處理器來同時使用並行方式來執行這兩個執行緒,發現到結果都是相同的,執行結果是正確的,而且,所花費的時間與使用單一執行緒的同步執行方式差不多,大約要花費 7.5秒的時間。
        +

        不過,雖然這裡使用了多執行緒方式的方式來執行,可以卻因為僅有一個處理器可以使用,雖然執行結果是正確的,發現到是沒有任何可以提升執行效能的幫助。

        發現問題2 : 多邏輯處理器會發生執行結果是錯的

        當指定了兩個以上的邏輯處理器來執行這個加一與減一的多執行緒程式,其共通的結果就是,最終的計算結果數值是錯誤的,因此,只要結果是不正確的,就算程式跑得再快,也是沒有用的。
        不過,從上面的執行結果發現到,在指定使用 10101000 3 個邏輯處理器的時候,竟然會比使用 10100000 邏輯處理器所花費的時間更多,竟然花費多達一倍以上的時間,不過,當使用了 11000000 這樣的兩個邏輯處理器的時候,卻得到僅需要 6.7 秒的時間,雖然這樣的執行效能比起同步執行結果少了 1 秒鐘,但是,因為結果是不正確的,跑得再快也沒用。
        另外,也發現到同樣的指定 3 個邏輯處理器 ,這裡使用 11100000 比起使用 10101000 明顯快多了(但是,他們的執行結果都是不正確的),這樣產生的問題,歡迎大家可以在留言版中,留下各位的看法,進行討論看看。
        最後,就算使用了所有的 8 個邏輯處理器,11111111,執行速度也沒有比起同步單一執行緒的執行速度快,而且這樣的結果也是不正確的。