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,執行速度也沒有比起同步單一執行緒的執行速度快,而且這樣的結果也是不正確的。



        2019年11月23日 星期六

        .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 1 在單一執行緒下,同步執行加一與減一方法

        .NET C# 單執行緒 同步 多執行緒 非同步 執行緒同步 Synchronization 邏輯處理器數量 設計探討 : Part 1 在單一執行緒下,同步執行加一與減一方法

        在這篇文章所提到的專案原始碼,可以從 GitHub 下載

        進行單一執行緒的測試

        在我這台電腦主機上,使用的是 Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz,其具有四核心與超執行緒 Hyper Threading 的技術,因此,對於作業系統而言,將會看到這台電腦具有八核心的能力。
        在這裡,將會進行這樣的計算作業,也就是 加一 +1 與 減一 -1
        C Sharp / C#
        class AddSub
        {
            public static int counter = 0;
            public void Adds()
            {
                for (int i = 0; i < int.MaxValue; i++)
                {
                    counter++;
                }
            }
            public void Subs()
            {
                for (int i = 0; i < int.MaxValue; i++)
                {
                    counter--;
                }
            }
        }
        
        如同上面的程式碼,在此設計了一個類別 AddSub ,該類別提供了兩個執行個體方法與一個靜態變數,第一個物件方法 Adds 會執行一個迴圈,將會跑 int.MaxValue 次,每次回圈內將會執行 counter++ 這個表示式,這裡的 counter 是一個靜態變數,但是 counter 的數值異動卻是由執行個體方法來去變更的;第二個物件方法 Subs 會執行一個迴圈,將會跑 int.MaxValue 次,每次回圈內將會執行 counter-- 這個表示式。也就是說 Adds() 這個方法將會再回圈內每次都進行加一的動作,而對於 Subs() 這個方法將會在回圈內每次都進行減一的動作。
        現在,首先要進行的測試目的為將會是要進行僅有一個執行緒的執行結果測試
        C Sharp / C#
        static void SyncAddSub()
        {
            AddSub addSub = new AddSub();
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            addSub.Adds();
            addSub.Subs();
            stopwatch.Stop();
            Console.WriteLine($"Counter={AddSub.counter}, {stopwatch.ElapsedMilliseconds:N0}ms");
        }
        
        透過上面的程式碼,設計出 SyncAddSub() 方法,在這個方法內,將會使用 Stopwatch 型別提供計算執行所需要花費時間工具,當計時器開始運作的時候,便會開始同步執行 addSub.Adds(); & addSub.Subs(); 這兩個加一與減一計算工作,最後將會顯示 AddSub.counter 這個靜態變數的值為多少 (當然,正確的結果應該是要為 0) 和所耗費的時間,單位式 ms。
        由於大多數的電腦大都具有多CPU或者多核心的能力,也就是說在這台電腦上將會有多個處理器可以使用,如此,便可以採用平行處理的方式,大幅提升整體電腦執行效能。
        為了要了解到上面設計的單一執行緒下執行的程式,若在具有不同數量的處理器下來執行,會有甚麼樣的變化,在這裡將會透過 Process.GetCurrentProcess().ProcessorAffinity = (IntPtr) 0b10000000 ; 這樣的敘述,來指定這次執行的 .NET 程式,僅能夠限制使用多少顆處理器數量,從這個例子中,經會限制這次執行的程式,僅能夠使用一顆處理器。
        為了要方便測試,這裡將會把所有需要測試的步驟,都設計在同一個專案程式碼內,ThreadSynchronization,透過者個專案程式,僅需要在執行的時候,提供不同的引數,就可以做到不同的執行效果,底下是使用方式說明
        使用方式 : ThreadSynchronization 多執行緒模式 計算方式 使用CPU模式
        多執行緒模式 : Yes , No
        計算方式 : NoLock , UserModeLock , UsingNETLock , NoLockByLocal
        使用CPU模式 : 1000000 , 11000000 , 10100000 , 11110000 , 10101000
        
        第一個參數將會指定是否要使用多執行緒的方式來執行測試,也就是加一這個方法將會在執行緒 Thread A 中來執行,而減一這個方法將會安排在執行緒 Thread B 中來執行;若在此提供的參數值為 no,則表示加一與減一這兩個方法,都會在同一個執行緒來執行。
        第二個參數將會指定要使用哪種 執行緒 同步 Thread Synchronization 機制來設計出執行緒安全 Thread Safe 的程式碼,也就是同樣的程式碼,在多執行緒環境下跑出來的結果,不論執行幾次,執行結果都是可以預期的,也就是結果將會是正確的,沒有模稜兩可問題產生;其中 NoLock 將會不使用任何的執行緒同步機制,當然,將會有可能造成執行緒不安全的問題、UserModeLock 將會使用 User Mode 使用者模式下的執行緒同步機制,這裡將會使用 Interlocked 這個類別來做到、UsingNETLock 這個參數指定使用 C# lock 關鍵字來做到關鍵區域 Critical Section 關鍵區域在同一個時間內,僅會有一個執行緒可以執行這段程式碼、NoLockByLocal 將會在多執行緒執行過程中,當在迴圈跑的時候,不會去變更共用靜態變數,而是在迴圈跑完之後,才會來將累加結果,更新到共用靜態變數內,而在這裡將會透過 Interlocked 這個類別來做到 執行緒安全 的目的。
        第三個參數則是用來指定所要使用的處理器 Processor 的數量,要使用這個參數是要確認,當成是於單執行緒 同步執行 或者多執行緒 非同步執行下,若使用不同的處理器數量,是否會有造成甚麼影響;當使用了 Process.GetCurrentProcess().ProcessorAffinity = (IntPtr) 0b10000000 ; 這個敘述,表示指定使用該核心內的一個處理器來進行運算,並不是使用到電腦上所有可用的處理器,而當使用了 Process.GetCurrentProcess().ProcessorAffinity = (IntPtr) 0b11000000 ; ,則設定使用同一個處理器硬體核心,不過,對於這裡指定兩個處理器,將會採用 Hyper-Threading 技術來運行,根據 Intel 的說明,當採用 Hyper-Threading 技術,可以提升約 20% ~ 30% 的執行效能;若使用了 Process.GetCurrentProcess().ProcessorAffinity = (IntPtr) 0b10100000 ; 敘述,則是指定了兩個核心處理器作為此次程式要運算的處理器。
        現在,請將這篇文章所提到的範例程式,請先使用 Release 建置模式來建置這個專案,接著,下達這樣的指令 ThreadSynchronization no NoLock 10000000
        這裡將會先來檢測僅使用單一執行緒來進行加一與減一計算工作,不過,這裡將會指定使用不同的處理器數量,底下是測試結果(在不同的電腦上,因為 CPU 硬體規格不盡相同,所以,可能需要依據讀者本身的可用處理器數量,自行調整第三個參數的內容。
        想要確認可用處理器數量,可以打開 Windows 電腦的工作管理員,切換到 [效能] 標籤頁次,在該頁次右下方將會看到 [邏輯處理器] 這個文字標籤的右邊,就是這台電腦上可用的處理器數量,在這台電腦上實際上為單一一顆CPU硬體,但是具有 4 Core 四核心,而且每個核心都有採用 Hyper Threading 的技術,因此,對於整體電腦而言,將會有 8 顆處理器數量,也就是這裡所提到的 邏輯處理器 數量。
        底下將會指定在單一執行緒下,同步執行加一與減一方法,並且指定在不同的邏輯處理器數量下的執行結果。
        ThreadSynchronization no NoLock 10000000
        Counter=0, 7,515ms
        
        ThreadSynchronization no NoLock 10100000
        Counter=0, 7,477ms
        
        ThreadSynchronization no NoLock 10101000
        Counter=0, 7,458ms
        
        ThreadSynchronization no NoLock 11000000
        Counter=0, 7,410ms
        
        ThreadSynchronization no NoLock 11100000
        Counter=0, 7,468ms
        
        ThreadSynchronization no NoLock 11110000
        Counter=0, 7,497ms
        
        ThreadSynchronization no NoLock 11111111
        Counter=0, 7,670ms
        
        從上面的執行結果可以看的出來,若在單一執行緒下,同步執行加一與減一方法,最終的計算結果,也就是 Counter 這個靜態變數的內容將都會是 0,這是正確與可以預期的結果,而且不論使用多少的邏輯處理器數量,所得到的結果都是相當的;這也就是說,若設計的程式採用單一執行緒的同步方式來執行,將不會有執行緒不安全的問題,而且執行上所花費的時間將不會明顯的影響。