2019年9月1日 星期日

C# 執行緒 Thread / ThreadPool 的建立、執行、傳遞參數、結束、回傳值、取消、例外異常 之使用教學

C# 執行緒 Thread / ThreadPool 的建立、執行、傳遞參數、結束、回傳值、取消、例外異常 之使用教學

在這篇文章中,將會來說明如何在 .NET C# 開發環境之下(不論是在 .NET Framewor or .NET Core or Windows Form or WPF or Xamarin or ASP.NET or ASP.NET MVC or ASP.NET Core 下),能夠建立與使用 Thread 執行緒的操作過程教學文章。首先將會先建立兩個方法,這兩個方法分別是執行迴圈 500 次,分別在螢幕上輸出 * 與 呼叫端所傳入的 - 這兩個符號,程式碼如下所示,其中在 MyMethod2 這個方法中,將會接收一個型別為 object 的 message 參數,這裡要設定參數型別為 object,這是因為等下在建立執行緒物件的時候,要傳入一個執行緒要執行的委派方法,其參數型別必須是 object 型別,因此,最後會使用 Console.Write(message.ToString()); 敘述將這個參數物件值顯示在螢幕上。
C Sharp / C#
static void MyMethod1()
{
    for (int i = 0; i < 500; i++)
    {
        Console.Write("*");
    }
}
static void MyMethod2(object message)
{
    for (int i = 0; i < 500; i++)
    {
        Console.Write(message.ToString());
    }
}
接著,將會使用這兩個方法,開始說明與進行教學,讓大家了解到如何在 C# 程式語言內,操作與使用執行緒這個系統資源,進行非同步程式設計。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

使用同步程式設計來呼叫這兩個方法

首先,將先使用同步程式設計方式,呼叫這兩個方法,在 使用同步方式呼叫() 方法內,將會看到會先執行 MyMethod1() 方法,接著在執行 MyMethod2() 方法。
C Sharp / C#
static void 使用同步方式呼叫()
{
    MyMethod1();
    MyMethod2("-");
}
由於這樣的程式碼是使用同步方式來設計與執行,因此,將會依照 使用同步方式呼叫() 方法內所撰寫的敘述逐一執行下去,所以,當執行這個程式碼的時後,將會看到螢幕輸出如同下面的內容。也就是說,在螢幕上會先看到有 500 個 * 字元出現,接著會有 500 - 字元出現,不過,因為這是採用同步程式設計,所以,這樣的執行結果將會滿足與符合當初設計上的需要。
********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
請注意,在這裡先提出一個需求,若使用同步程式方法設計,當在呼叫這兩個方法的時候,要怎麼做到中止這兩個方法的繼續執行需求呢?這樣的需求在後面的使用執行緒 Thread 的非同步執行方式中,也會有著同樣的需求。

建立執行緒 - 使用 Thread 類別來建立一個執行緒物件與傳遞參數用法

若你的電腦具有多核心處理器或者具有多個處理器的設備,使用多執行緒的程式設計,可以讓你享受到不只是併行處理的多工好處,也會享受到可以改善執行效能的平行處理優點,因為在平行處理的環境下,多核心處理器或者多處理器的電腦,可以同時執行許多執行緒,也就是說,若該電腦總共具 8 顆邏輯處理器,那就代表這台電腦可以同時執行 8 個執行緒內的程式碼。
不過,請不要以為將你的程式碼切割成為可以在 8 個執行緒執行的程式碼,如此,就可以在有 8 個邏輯處理器的作業統下,提升 8 倍的執行速度
想要建立一個執行緒物件,必須要先產生一個 Thread 類別的執行個體,而當使用 new 運算子要產生一個 Thread 類別的執行個體,對於 Thread 類別的建構函式,需要提供一個 ThreadStart 委派型別的委派方法 或者 ParameterizedThreadStart 委派型別的委派方法;前者的委派型別宣告為 public delegate void ThreadStart() ,只要一個方法沒有回傳值與沒有任何參數使用,都可以作為建立該 Thread 類別物件的委派方法,後者的委派型別宣告為 public delegate void ParameterizedThreadStart(object obj) 也就是需要一個參數,其型別必須為 object 型別,這也就是為什麼對於剛剛定義的 MyMethod2 方法,需要宣告該方法需要傳送一個 object 的引數到該方法內。
在底下,將會看到使用 Thread thread1 = new Thread(MyMethod1);Thread thread2 = new Thread(MyMethod2); 敘述,分別建立兩個執行緒物件,在這裡要特別注意的是,當建立起一個 Thread 物件,該執行緒是在尚未啟動執行的狀態,若想要命令該執行緒開始執行,則需要取得該執行緒物件,呼叫該執行個體方法 Start(),就可以讓該執行緒開始執行。對於當初所建立的執行緒所呼叫的建構函式,所指派的委派方法,是屬於 ThreadStart 委派型別,可以直接呼叫 Start() 方法即可,可是,對於所指定的執行緒要使用的委派方法,也就是符合 ParameterizedThreadStart 委派型別,就需要使用 public void Start(object parameter); 這個多載方法,提供一個物件到 Start 方法引數內,這樣,所指定的委派方法,這裡是 MyMethod2 就會接收到這個參數值,當然,在這個方法 MyMethod2 就會把這個參數值顯示在螢幕上。
由於無法得知這兩個執行緒何時會都完成運算,所以,在這裡將會使用 Thread.Sleep(2000) 敘述,讓主執行緒暫時休息 2 秒鐘,等候這兩個執行緒執行完成;底下是自行建立執行緒物件並併行執行的結果內容。
請注意,若將敘述 Thread.Sleep(2000) 從 建立執行緒_使用Thread類別來建立一個執行緒物件() 方法內移除之後,請問會發生甚麼問題?另外,究竟要休息多久的時間,才是一個好的設計,例如,是要休息一秒鐘還是三秒鐘呢?
C Sharp / C#
private static void 建立執行緒_使用Thread類別來建立一個執行緒物件()
{
    Thread thread1 = new Thread(MyMethod1);
    Thread thread2 = new Thread(MyMethod2);
    thread1.Start();
    thread2.Start("-");

    Thread.Sleep(2000);
}
從執行結果可以看出與使用同步程式設計方式所得到的執行結果是不同的,在這裡可以看出執行緒1與執行緒2似乎每次僅會獲得 CPU 一小片段的執行時間,當該執行緒的CPU使用時間用完之後,將會強制退出暫時無法使用 CPU,作業系統會將 CPU 使用權力分配給其他的執行緒,不過,這個執行緒也是僅能夠執行一小片段的時間;因此,可以看出執行緒1與執行緒2是分別交錯的在運行中,由於每次使用 CPU 的時間都很短暫,而且馬上就可以分配到 CPU 使用權限,這樣的過程讓使用者覺得這兩個執行緒是同時在執行中,形成一個多工執行的樣貌。
***********---------------------------***********************************************************************************************************************************************************************************************************--------------------------------------------------------------------------------------------------------------------------------------------------------------****************************************************************************************************************************************************************************----------------------------------------------------------------------------------------------------------------------------------------------******************************************************************************************************************-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
這裡所提到的多執行緒的程式設計,是最基本的使用方式:建立一個執行緒物件,傳送一個委派方法,呼叫該執行緒物件的 Start 方法,如此,該執行緒就已經開始在系統中執行了;想要傳遞參數給該執行緒,請使用 public void Start(object parameter); 這個多載方法

透過 執行緒集區 ThreadPool 取得一個執行緒與傳遞參數用法

由於臨時要系統產生出一個執行緒物件出來,是相當耗費系統資源成本的,為了要讓執行緒物件可以達到最大可重複使用效率,避免過多的執行緒在系統中,造成其他的副作用影響,因此,有了執行緒集區這樣的解決方案。當 .NET 程式啟動之後,執行緒集區內就已經預設產生了許多執行緒,至於是有多少執行緒,這要看當時執行程式的電腦硬體上有多少個邏輯處理器而定。所以,有了執行緒集區之後,當在 .NET 程式中想要取得一個執行緒進行多工、非同步運算,就不再需要直接 new 一個執行緒執行個體出來,可以呼叫靜態方法 ThreadPool.QueueUserWorkItem 即可。
根據這個函式的宣告,有兩種使用方式,分別是 public static bool QueueUserWorkItem(WaitCallback callBack) 與 public static bool QueueUserWorkItem(WaitCallback callBack, object state)。前者適用於指定一個執行緒要執行的委派方法,後者則是同樣要指定一個執行緒要執行的委派方法,不過,可以指定一個物件要傳入到該執行緒委派方法內。而對於 WaitCallback 的委派型別宣告為 public delegate void WaitCallback(object state),因此,當要透過執行緒集區取得一個可用執行緒的時候,所傳送過去的委派方法,必須是一個具有可以讀取一個 object 型別參數的函式,因此,對於原先的 MyMethod1 因為沒有任何參數的定義,無法直接用於 ThreadPool.QueueUserWorkItem 方法內;在這裡將會轉個彎來使用這個方法,在底下的程式碼中,在呼叫 ThreadPool.QueueUserWorkItem 的時候,所提供的委派方法是一個 Lambda 匿名委派方法,在這個 Lambda 方法內會進一步的呼叫 MyMethod1,這樣就可以解決此一問題。
使用執行緒集區取得一個執行緒與使用 Thread 類別建立起一個執行緒物件最大的差異將會是,當透過執行緒集區取得的執行緒,該執行緒將會立即開始執行,而對於後者,使用 Thread 類別建立起來的物件,就需要呼叫該 Thread 物件的 Start() 方法,這樣,該執行緒才會開始執行起來。底下是使用執行緒集區取得兩個執行緒,螢幕輸出結果。
C Sharp / C#
private static void 透過執行緒集區ThreadPool取得一個執行緒與傳遞參數用法()
{
    // 要從執行緒集區內取得一個執行緒,需要提供一個
    //   public delegate void WaitCallback(object state);
    // 委派函式簽章,也就是該委派方法必須要有一個參數
    // 因此,在這裡透過 Lambda 匿名委派方法來呼叫 MyMethod1() 方法
    ThreadPool.QueueUserWorkItem(_ => MyMethod1());
    // 使用執行緒集區取得的執行緒,若要傳遞引數,請接著委派方法之後傳入
    ThreadPool.QueueUserWorkItem(MyMethod2, "-");

    Thread.Sleep(2000);
}
因為 MyMethod1 與 MyMethod2 分別在兩個執行緒內執行,因此,他們會同時在系統中執行,每個執行緒僅能夠使用一小片段的 CPU 時間 time slicing,將會形成這樣的執行結果,而且,重複執行幾次,將會看到每次執行的輸出結果都不會相同
*************----------------------------------------------------------------------------------------------------------------------------------------------------------------*****************************************************************************************************************************************************************************-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*****************************************************************************************************************************************************************************-------------------------------------------------------------------------------------------------*********************************************************************************************************************************************--------------------------------------------------

當使用執行緒類別建立執行個體,如何等待該執行緒執行完成

在前面的程式碼中,因為無法確定執行緒需要多久的時間才能夠執行完畢,因此,會在啟動兩個執行緒執行之後,在主執行緒內休息兩秒鐘,才會結束這個應用程式的執行,可是,在正常的開發程式中是無法使用這樣的方式來進行程式碼的設計;因此,想要知道該執行緒是否已經成功執行完畢,需要使用 Thread.Join() 這個執行個體方法來進行封鎖 Block 等候,也就是說,當執行這個方法之後,若所等待的執行緒一時之間無法結束執行,呼叫 Join() 方法的執行緒也無法繼續往下執行,必須等到該執行執行完畢之後,才能夠繼續往下執行。
在底下的程式碼中,將會呼叫 thread1.Join();thread2.Join(); 方法來依序等候執行緒1與執行緒2執行完畢。
C Sharp / C#
private static void 當使用執行緒類別建立執行個體_如何等待該執行緒執行完成()
{
    Thread thread1 = new Thread(MyMethod1);
    Thread thread2 = new Thread(MyMethod2);
    thread1.Start();
    thread2.Start("-");

    thread1.Join();
    thread2.Join();
}

透過執行緒集區取得的執行緒,如何得知該執行緒已經結束執行了

對於透過執行緒集區來使用 ThreadPool.QueueUserWorkItem 取得一個可用執行緒,開始執行一個委派方法,這個 ThreadPool.QueueUserWorkItem 方法是不會回傳一個執行緒物件的,那麼,要如何得知這個從執行緒集區取得的執行緒,已經執行完成了呢?因為,無法回傳一個執行緒物件,就無法呼叫該執行緒執行個體的 Join() 方法。
對於從執行緒集區取得的執行緒,當然可以在該執行緒的委派方法內使用 Thread.CurrentThread 屬性取得當前的執行緒物件,可是,這並不是一個很好的做法,在此,將會展示使用 WaitHandle 類別來做到執行緒同步的需求;WaitHandle類別會封裝原生作業系統同步處理控制碼, 並用來代表執行時間中允許多個等候作業的所有同步物件,而 衍生自WaitHandle的類別包括:Mutex 類別、類別EventWaitHandle和其衍生的類別, AutoResetEvent 以及ManualResetEvent、Semaphore 類別。當然,也可以選擇其他的執行同步處理機制做到相同的效果。
首先,對於原先的 MyMethod1, MyMethod2 兩個委派方法,需要稍作修改,在這裡將會使用底下兩個新寫的兩個委派方法, MyMethod21, MyMethod22 這兩個方法,這兩個方法都會需要接收一個參數,這個參數將會是一個 AutoResetEvent 的物件,當在這個委派方法執行完畢之後,將會呼叫 AutoResetEvent 執行個體的 Set() 方法 AutoResetEvent.Set,該方法代表 將事件的狀態設定為收到信號,允許一個或多個等待中的執行緒繼續執行;經過這樣的設計,就可以透過 WaitHandle 得知是否已經有任何執行緒完成的訊號送出,以便判斷是否要繼續往下執行下去。
另外,在這裡將會把 MyMethod21 的迴圈設定為 800 次,模擬兩個方法需要花費不同的時間長度才會執行完畢
C Sharp / C#
static void MyMethod21(object state)
{
    AutoResetEvent are = (AutoResetEvent)state;
    for (int i = 0; i < 800; i++)
    {
        Console.Write("*");
    }
    are.Set();
}
static void MyMethod22(object state)
{
    AutoResetEvent are = (AutoResetEvent)state;
    for (int i = 0; i < 500; i++)
    {
        Console.Write("-");
    }
    are.Set();
}
另外,需要建立一個 AllWaitHandles 陣列集合物件,用來儲存 WaitHandle 的物件,這裡將會使用 AutoResetEvent 這個物件,當在產生這個物件的時候,在建構函式內傳入該訊號的預設值,這裡傳入 false,表示尚未發出任何訊號。
另外,當透過執行緒集區 ThreadPool.QueueUserWorkItem 取得兩個執行緒的時候,需要傳送 WaitHandle 物件到該執行緒的委派方法內,也就是 MyMethod21, MyMethod22 方法內,這樣,當執行緒分別執行這兩個方法的時候,一旦執行完成之後,將會使用 AutoResetEvent.Set 方法發出一個訊號,通知這個執行緒已經執行完成了。
而在主執行緒,使用了 WaitHandle.WaitAll(AllWaitHandles) 這個靜態方法來等候這兩個 AutoResetEvent 都已經執行過 Set 方法了,也就是說這兩個執行緒都已經執行完畢了,這樣,主執行緒就可以繼續往下執行下去。
C Sharp / C#
static WaitHandle[] AllWaitHandles = new WaitHandle[]
{
    new AutoResetEvent(false),new AutoResetEvent(false)
};
private static void 透過執行緒集區取得的執行緒_如何得知該執行緒已經結束執行了()
{
    ThreadPool.QueueUserWorkItem(MyMethod21, AllWaitHandles[0]);
    ThreadPool.QueueUserWorkItem(MyMethod22, AllWaitHandles[1]);
    WaitHandle.WaitAll(AllWaitHandles);
}
底下將會是該程式碼的執行結果。在此,將會看到當呼叫了 WaitHandle.WaitAll(AllWaitHandles) 方法之後,主執行緒進入封鎖 Block 狀態,必須等到兩個執行緒都執行完畢之後,才會繼續執行。
-----------*****************************************************************************************************************************************----------------------------------------------------------------------------------------------------------------------------------------------------------------------****************************************************************************************************************************************************************---------------------------------------------------------------------------------------------------------------------------------------------------------------------------******************************************************--------------------------------------------------------------------------------------------------------------------------------------------------------*****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************

如何取得執行緒執行完成之後的執行結果回傳值

不論使用自己建立一個執行緒或者透過執行緒集區來取得一個執行緒,若想要得到該執行緒執行的回傳結果,不像同步程式設計那樣的簡單。
首先,要建立兩個新的委派方法, MyMethod23, MyMethod24 ;在這兩個方法內同樣的是使用 AutoResetEvent 物件來送出一個該執行緒結束執行的訊號,並且在送出訊號之前,將該執行緒的計算結果指定到一個靜態變數內,使用靜態變數的原因是因為無法很容易地在不同執行緒內存取不同物件值,當然,也可以使用其他的方式,例如,設計一個 callback 方法,當該執行緒執行完畢之後,透過 callback 方法,將執行完成的結果送出去。
在底下的程式碼中,可以看到在 MyMethod23 方法執行完成前,會將回傳的物件值設定到 Result1 靜態變數內,而在 MyMethod24 方法執行完成前,會將回傳的物件值設定到 Result2 靜態變數內,最後透過 AutoResetEvent.Set() 方法送出訊號,告知用戶端的執行緒,該執行緒已經執行完畢了。
C Sharp / C#
static void MyMethod23(object state)
{
    AutoResetEvent are = (AutoResetEvent)state;
    for (int i = 0; i < 800; i++)
    {
        Console.Write("*");
    }
    Result1 = 800;
    are.Set();
}
static void MyMethod24(object state)
{
    AutoResetEvent are = (AutoResetEvent)state;
    for (int i = 0; i < 500; i++)
    {
        Console.Write("-");
    }
    Result2 = 500;
    are.Set();
}
在用戶端這裡,使用執行緒集區來取得兩個執行緒,並且開始執行這兩個執行緒委派方法,之後使用 WaitHandle.WaitAll(AllWaitHandles); 方法來進行 Block 封鎖等候,當兩個執行緒執行完畢且都送出訊號之後,主執行緒後面的程式碼將會繼續執行,在這裡會把兩個執行緒的執行結果顯示在螢幕上。
C Sharp / C#
static int Result1, Result2;
private static void 如何取得執行緒執行完成之後的執行結果回傳值()
{
    ThreadPool.QueueUserWorkItem(MyMethod23, AllWaitHandles[0]);
    ThreadPool.QueueUserWorkItem(MyMethod24, AllWaitHandles[1]);
    WaitHandle.WaitAll(AllWaitHandles);
    Console.WriteLine();
    Console.WriteLine($"MyMethod23 的執行結果為 {Result1}");
    Console.WriteLine($"MyMethod24 的執行結果為 {Result2}");
}
底下是這個範例程式碼執行後的螢幕輸出內容。
*****---------------------------------------------------------------------------------------------------------------------------------------------------------******************************************************************************************************************************************************************----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*************************************************************************************************************************************************************************************************---------------------------------------------------------------------------------------------------------------------------------------------------************************************************************************************************************************************************************************************----------------------********************************************************************************************************************************************************************************************************************************************************************
MyMethod23 的執行結果為 800
MyMethod24 的執行結果為 500

當使用執行緒類別建立執行個體,如何取消正在執行的執行緒

當想要取消一個正在執行的執行緒,相信很多人都會使用 Thread.Abort() 方法,這將會於被叫用的所在執行緒中引發 ThreadAbortException,開始處理執行緒的結束作業;當使用這個 Abort 方法的時候,可能會導致靜態的執行程式無法執行或防止非受控資源的釋放,在這裡來看看如何使用這樣的做法。
想要使用 Thread.Abort 方法,第一個先決條件當人是需要先取得該執行緒物件,才能夠呼叫該執行緒執行個體上的 Abort 方法,對於執行緒是透過建立一個 Thread 類別物件的作法,當然是可以取得這個執行緒物件,不過,對於執行緒集區取得的執行緒,就需要透過其他的方式來實作出取消執行緒執行的功能。
在此先建立兩個執行緒會用到的委派方法,在這裡將會建立一個無窮迴圈,列印出傳送進來的參數值,接著休息 30ms 的時間。
C Sharp / C#
static void MyMethod25(object message)
{
    while (true)
    {
        Console.Write("*");
        Thread.Sleep(30);
    }
}
static void MyMethod26(object message)
{
    while (true)
    {
        Console.Write("-");
        Thread.Sleep(30);
    }
}
在主執行緒,呼叫端,將會建立兩個執行緒物件,接著啟動執行這兩個執行緒,然後主執行緒先休息 1 秒鐘,對執行緒2物件呼叫 Abort() 方法,此時,執行緒2 將會終止執行,也就是螢幕上再也不會顯示出 - 這個字元;主執行緒接著休息 3 秒鐘,此時,螢幕上會持續只顯示 * 字元,在 3 秒鐘之後,會呼叫執行緒1 的 Abort() 方法,這樣,執行緒1也終止執行了,螢幕上再也顯示不出任何內容了。
C Sharp / C#
private static void 當使用執行緒類別建立執行個體_如何取消正在執行的執行緒()
{
    Thread thread1 = new Thread(MyMethod25);
    Thread thread2 = new Thread(MyMethod26);
    thread1.Start("*");
    thread2.Start("-");

    Thread.Sleep(1000);
    thread2.Abort();
    Thread.Sleep(3000);
    thread1.Abort();
}
底下是執行結果
*--*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-**************************************************************************************************

當執行緒內的程式碼拋出例外異常,會有甚麼問題產生

最後,在設計多執行緒非同步程式碼的時候,需要特別注意在執行緒內執行的委派方法內,是不能夠拋出任何例外異常的,因為,在任何執行緒下若有拋出任何例外異常(只有少數幾個是例外),將會造成這個應用程序終止執行;當發生這樣的問題時候,通常是最難進行除錯的時候,因為,想要抓出問題是在哪個執行緒下產生問題,不是這麼容易的。
現在,這裡僅會建立一個執行緒,在這個範例中,將會透過執行緒集區取得一個執行緒,該執行緒將會執行底下的 MyMethod27 委派方法,該方法將會顯示 @ 字元,當顯示到第 301 個字元的時候,將會拋出一個例外異常,在這裡將會使用 throw new Exception("喔喔,系統拋出例外異常") 來手動拋出例外異常。
C Sharp / C#
static void MyMethod27(object message)
{
    for (int i = 0; i < 1000; i++)
    {
        Console.Write(message.ToString());
        if (i == 300)
        {
            throw new Exception("喔喔,系統拋出例外異常");
        }
    }
}
在主執行緒端,會透過背景執行緒執行 MyMethod27 方法,緊接著休息 5 秒鐘,沒有多久時間,MyMethod27 將會拋出例外異常,這個時候,將會造成整個處理程序異常終止,而在主執行緒的任何程式碼將再也無法繼續執行了。
C Sharp / C#
private static void 當執行緒內的程式碼拋出例外異常_會有甚麼問題產生()
{
    ThreadPool.QueueUserWorkItem(MyMethod27, "@");
    Thread.Sleep(5000);
    Console.WriteLine("因為執行緒拋出例外異常,這行文字永遠無法顯示在螢幕上");
}
這裡是執行結果
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
未處理的例外狀況: System.Exception: 喔喔,系統拋出例外異常
   於 ThreadUsage.Program.MyMethod27(Object message) 於 D:\Vulcan\GitHub\CSharp2019\ThreadUsage\ThreadUsage\Program.cs: 行 179
   於 System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
   於 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   於 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   於 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   於 System.Threading.ThreadPoolWorkQueue.Dispatch()
   於 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()




2019年8月29日 星期四

在 .NET Core 下, 執行緒集區 Thread Pool的執行緒生成與管理測試

在 .NET Core 下, 執行緒集區 Thread Pool的執行緒生成與管理測試

這篇文章將會透過一個範例程式碼,了解到執行緒集區如何智慧型的管理其持有的執行緒;在所測試的電腦,是個 4 核心且具有超執行緒 Hyper-Threading 的 CPU,因此,對於作業系統而言,將會擁有 8 顆邏輯處理器的狀態,因此,對於一個 .NET 應用程式而言,當這個 .NET 應用程式啟動之後,對於執行緒集區內,將會自動建立起八個 CPU Bound 使用的執行緒與八個 I/O Port 使用的執行緒。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [ThreadPool Class] 的使用方式



因為,當應用程式需要透過執行緒集區取得多於預設持有持行續的時候,會有什麼狀況發生,進行測試,在這篇文章所使用的開發框架是 .NET Core Console專案。
首先將會使用 FindDefaultThreadOnThreadPool() 方法,取得執行緒集區內所有受管理執行續 ID,並且記錄下來,這樣,等下的測試程式碼,才能夠得知透過執行緒集區取得的執行緒,究竟是執行緒集區原本就預先持有的,還是,執行緒集區另外心產生出來的呢?
接著,這個測試程式將會於不同時間點內,呼叫這個 EmulateMoreThreads 方法,該方法將會透過執行緒集區取得 11 個執行緒,也就是說遠遠超過執行緒集區所預先持有的執行緒,不過,對於執行緒集區並不會受到任何影響,因為超過的 3 個執行緒,執行緒集區將會以每 0.5~1秒鐘的時間,逐漸地建立起這些額外需要的執行緒。另外,對於這這個 EmulateMoreThreads 方法內所取得的執行緒,將會使用 Thread.Sleep 來休息一段時間,才會準備結束執行。
對於 EmulateMoreThreads 方法所取得的執行緒,若該執行緒是該 .NET 應用程式一啟動就在執行緒集區內所配置好的執行緒,將會顯示類似底下的文字,也就是會看到有 ** 兩個星號的符號
要求執行緒作業(5) ** Thread9 從執行緒集區取得該執行緒 10:31:38.9432525
若當前執行的執行緒是執行緒集區新產生出來的,將不會看到有 ** 兩個星號的符號
要求執行緒作業(9) Thread12 是執行緒集區額外新建立的執行緒 10:31:39.9499312
將者將會進行四次的 EmulateMoreThreads 方法呼叫,每次都會使用不同的間隔時間。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

FindDefaultThreadOnThreadPool 蒐集執行緒集區內的所有受管理執行緒 ID 清單

一開始將會執行 FindDefaultThreadOnThreadPool() 方法,在這個方法內,會建立一個迴圈,依據現在執行緒集區內持有的執行緒數量大小,執行一個迴圈,在該迴圈內會從執行緒集區內取得一個執行緒,直到所有執行緒集區內的執行緒都被取出來。
可是,這裡有個需求,那就是當該迴圈內的執行緒之委派方法都執行之後,需要等候訊號通知,以便結束這些委派方法,也就是接著會將這些執行緒歸還到執行緒集區內,還有一個就是,要怎麼知道這些執行緒的委派方法都已經啟動與執行了,並且準備要讀取結束執行的訊號呢?
在執行緒所指定的委派方法內,將會使用 CountdownEvent 、 Semaphore 這個執行緒同步物件,在執行緒指定的委派方法內,將會先顯示一段字串,使用敘述 Console.WriteLine($"Thread{threadId} 已經從執行緒集區取得該執行緒"); ,接著使用 countdown.Signal() 這個方法,告知某個執行緒的委派方法已經成功執行了,接著將會執行 semaphore.WaitOne(); ,等候訊號通知;一旦接收到來自於 Semophore 的訊號,將會使用敘述 Console.WriteLine($"Thread{threadId} 已經歸還給執行緒集區"); 顯示字串,並且結束該委派方法的執行,也就是結束該執行緒的執行。
所以,在迴圈之外,將會使用 countdown.Wait() 來等候所有執行緒集區內的執行緒,都已經正常在執行過程中,只要每個執行緒都有執行到 countdown.Signal() 這個方法,則 countdown.Wait() 這個方法將會繼續往下執行,否則,將會持續使用封鎖式的等候。
緊接著將會使用 semaphore.Release(workerThreads) 方法呼叫,通知所有在等候 semaphore.WaitOne() 訊號的敘述,可以繼續往下執行了,只要 Release 方法一執行,所有的執行緒都會同時收到訊號通知,並且同時繼續往下執行。
在每個執行緒的委派方法內,最重要的一個工作就是要取得當前執行緒所使用的受管理執行緒 ID,這裡將會使用 ThreadsOnThreadPool.TryAdd(threadId, threadId) 敘述來執行,而 ThreadsOnThreadPool 這個物件的宣告型別為 ConcurrentDictionary<int, int>,是個可以用於多執行緒環境下的集合資料字典型別。

EmulateMoreThreads 顯示該執行緒是否為預設執行緒集區持有的執行緒

在這個方法內,將會透過 ThreadsOnThreadPool 這個物件,判斷當前從執行緒集區取得的執行緒,是否為應用程式一起動的時候,所擁有的執行緒,還是執行緒集區另外新產生出來的執行緒;若為原有的執行緒,將會顯示 ** 兩個星號文字,並且休息 defalutSleep ms 的時間,若為執行緒集區新建立的執行緒,則不會看到有任何星號文字出現,並且會休息 NewSleep ms 的時間

執行結果分析

這個測試程式一啟動之後,將會先執行 FindDefaultThreadOnThreadPool() 方法,該方法所執行的工作,前面已久描述了,接著,將會
第一次呼叫 EmulateMoreThreads 方法,從執行輸出結果可以看出,總共會透過執行緒集區取得 11 個執行緒,並且,其中執行緒 9,10,11 這三個執行緒分別間隔一秒鐘的時間,才從執行緒集區取得執行緒,並開始進行執行,其他八個執行緒幾乎是同一個時間就從執行緒集區內取得該執行緒,立即執行。會有這樣的差異那是因為對於這台測試電腦,任何 .NET 應用程式啟動的時候,預設執行緒集區將會擁有八個執行緒物件,因此,前八個要求取得執行緒的需求,可以很快地取得執行緒,後面三個則幾乎是每一秒鐘才會取得所需的執行緒。
接著休息 2 秒鐘,進行第二次呼叫 EmulateMoreThreads 方法,同樣的還是會透過執行緒集區取得 11 個執行緒,從執行結果可以看到出來,這 11 個執行緒的取得與執行委派方法的時間相當的接近,這是因為在剛剛第一次呼叫的時候,執行緒集區已經產生出額外三個執行緒,因此,現在執行緒集區內將會擁有 11 個執行緒,所以,現在當要向執行緒集區要求取得 11 個執行緒,執行緒集區將會立即配置這些執行緒給大家使用,沒有任何延遲問題。
現在將會有個疑問,這三個新建立、產生出來的執行緒,是否會一直存在於執行緒集區內呢?試想一個請況,若有個極端應用,有應用程式在同一個時間內向執行緒集區要求 1000 個執行緒,當然,執行緒集區會依序建立起這些執行緒物件,然而,是否在該應用程式結束之前,這 1000-8=992個執行緒,都會一直存在於系統中呢?
現在,休息 2 秒鐘,進行第三次呼叫 EmulateMoreThreads 方法,在呼叫該方法之前,將會先休息 30 秒鐘之後,才會開始執行這個方法。從執行輸出結果中可以看的出來,這裡所取得的執行緒 ID,與前面的步驟都不盡相同,而且,最後三個執行緒也是間隔一秒鐘之後才會取得;從這裡可以看的出來,之前要求執行緒額外產生的三個執行緒,似乎已經被執行緒集區回收掉了,也就是說,在休息 30 秒之後,執行緒集區內,僅剩下 8 個可用的執行緒可以立即配置與使用。
最後,再度休息 30 秒鐘,進行第四次的 呼叫 EmulateMoreThreads 方法,在呼叫該方法前,會先清空原先找出的執行緒集區內的預設執行ID清單,使用敘述 ThreadsOnThreadPool.Clear() 來完成,然後呼叫 FindDefaultThreadOnThreadPool() 方法重新建立此清單。從這次的執行輸出結果可以看出與第一次的執行結果相同,只不過所顯示的執行緒ID數值是不相同的。
底下是執行結果的所有輸出內容。
等候取得所有執行緒都從執行緒集區取得...
Thread6 已經從執行緒集區取得該執行緒
Thread10 已經從執行緒集區取得該執行緒
Thread4 已經從執行緒集區取得該執行緒
Thread7 已經從執行緒集區取得該執行緒
Thread11 已經從執行緒集區取得該執行緒
Thread8 已經從執行緒集區取得該執行緒
Thread9 已經從執行緒集區取得該執行緒
Thread5 已經從執行緒集區取得該執行緒
準備把取得的執行緒歸還給執行緒集區...
Thread5 已經歸還給執行緒集區
Thread9 已經歸還給執行緒集區
Thread7 已經歸還給執行緒集區
Thread11 已經歸還給執行緒集區
Thread4 已經歸還給執行緒集區
Thread6 已經歸還給執行緒集區
Thread8 已經歸還給執行緒集區
Thread10 已經歸還給執行緒集區


第 1 次,產生 11 執行緒請求


對於預先配置執行緒將休息 4000 ms, 對於新產生的執行緒將休息 4000 ms

要求執行緒作業(1) ** Thread10 從執行緒集區取得該執行緒 11:40:11.8510015
要求執行緒作業(8) ** Thread5 從執行緒集區取得該執行緒 11:40:11.8510139
要求執行緒作業(2) ** Thread8 從執行緒集區取得該執行緒 11:40:11.8510584
要求執行緒作業(4) ** Thread6 從執行緒集區取得該執行緒 11:40:11.8510709
要求執行緒作業(7) ** Thread9 從執行緒集區取得該執行緒 11:40:11.8510375
要求執行緒作業(5) ** Thread11 從執行緒集區取得該執行緒 11:40:11.8510057
要求執行緒作業(3) ** Thread4 從執行緒集區取得該執行緒 11:40:11.8510802
要求執行緒作業(6) ** Thread7 從執行緒集區取得該執行緒 11:40:11.8510473
要求執行緒作業(9) Thread12 是執行緒集區額外新建立的執行緒 11:40:12.8498194
要求執行緒作業(10) Thread13 是執行緒集區額外新建立的執行緒 11:40:13.8498459
要求執行緒作業(11) Thread14 是執行緒集區額外新建立的執行緒 11:40:14.8508899
要求執行緒作業(4) ** Thread6 準備結束執行 11:40:15.8544696
要求執行緒作業(1) ** Thread10 準備結束執行 11:40:15.8544696
要求執行緒作業(2) ** Thread8 準備結束執行 11:40:15.8544722
要求執行緒作業(8) ** Thread5 準備結束執行 11:40:15.8544744
要求執行緒作業(7) ** Thread9 準備結束執行 11:40:15.8555398
要求執行緒作業(5) ** Thread11 準備結束執行 11:40:15.8555436
要求執行緒作業(6) ** Thread7 準備結束執行 11:40:15.8564553
要求執行緒作業(3) ** Thread4 準備結束執行 11:40:15.8564554
要求執行緒作業(9)  Thread12 準備結束執行 11:40:16.8504807
要求執行緒作業(10)  Thread13 準備結束執行 11:40:17.8505184
要求執行緒作業(11)  Thread14 準備結束執行 11:40:18.8515355


第 2次,產生 11 執行緒請求


對於預先配置執行緒將休息 5000 ms, 對於新產生的執行緒將休息 5000 ms

要求執行緒作業(1) Thread14 是執行緒集區額外新建立的執行緒 11:40:20.8562061
要求執行緒作業(4) ** Thread7 從執行緒集區取得該執行緒 11:40:20.8562618
要求執行緒作業(5) ** Thread4 從執行緒集區取得該執行緒 11:40:20.8562618
要求執行緒作業(3) Thread12 是執行緒集區額外新建立的執行緒 11:40:20.8562659
要求執行緒作業(6) ** Thread11 從執行緒集區取得該執行緒 11:40:20.8562662
要求執行緒作業(2) Thread13 是執行緒集區額外新建立的執行緒 11:40:20.8562476
要求執行緒作業(7) ** Thread9 從執行緒集區取得該執行緒 11:40:20.8562734
要求執行緒作業(8) ** Thread5 從執行緒集區取得該執行緒 11:40:20.8562763
要求執行緒作業(9) ** Thread8 從執行緒集區取得該執行緒 11:40:20.8562824
要求執行緒作業(11) ** Thread6 從執行緒集區取得該執行緒 11:40:20.8562873
要求執行緒作業(10) Thread15 是執行緒集區額外新建立的執行緒 11:40:20.8562993
要求執行緒作業(4) ** Thread7 準備結束執行 11:40:25.8673359
要求執行緒作業(3)  Thread12 準備結束執行 11:40:25.8673823
要求執行緒作業(7) ** Thread9 準備結束執行 11:40:25.8673878
要求執行緒作業(8) ** Thread5 準備結束執行 11:40:25.8674149
要求執行緒作業(9) ** Thread8 準備結束執行 11:40:25.8674217
要求執行緒作業(10)  Thread15 準備結束執行 11:40:25.8674227
要求執行緒作業(5) ** Thread4 準備結束執行 11:40:25.8674644
要求執行緒作業(1)  Thread14 準備結束執行 11:40:25.8674045
要求執行緒作業(2)  Thread13 準備結束執行 11:40:25.8674931
要求執行緒作業(11) ** Thread6 準備結束執行 11:40:25.8675148
要求執行緒作業(6) ** Thread11 準備結束執行 11:40:25.8675064


第 3次,產生 11 執行緒請求,休息 30 秒


對於預先配置執行緒將休息 4000 ms, 對於新產生的執行緒將休息 4000 ms

要求執行緒作業(1) ** Thread11 從執行緒集區取得該執行緒 11:40:57.8726102
要求執行緒作業(2) Thread17 是執行緒集區額外新建立的執行緒 11:40:57.8737710
要求執行緒作業(3) Thread16 是執行緒集區額外新建立的執行緒 11:40:57.8738063
要求執行緒作業(4) Thread18 是執行緒集區額外新建立的執行緒 11:40:57.8739219
要求執行緒作業(5) Thread19 是執行緒集區額外新建立的執行緒 11:40:57.8745854
要求執行緒作業(6) Thread20 是執行緒集區額外新建立的執行緒 11:40:57.8750828
要求執行緒作業(7) Thread21 是執行緒集區額外新建立的執行緒 11:40:57.8765357
要求執行緒作業(8) Thread22 是執行緒集區額外新建立的執行緒 11:40:57.8769389
要求執行緒作業(9) Thread23 是執行緒集區額外新建立的執行緒 11:40:58.8734008
要求執行緒作業(10) Thread24 是執行緒集區額外新建立的執行緒 11:40:59.8744712
要求執行緒作業(11) Thread25 是執行緒集區額外新建立的執行緒 11:41:00.8755105
要求執行緒作業(1) ** Thread11 準備結束執行 11:41:01.8741022
要求執行緒作業(2)  Thread17 準備結束執行 11:41:01.8741131
要求執行緒作業(4)  Thread18 準備結束執行 11:41:01.8750861
要求執行緒作業(3)  Thread16 準備結束執行 11:41:01.8750870
要求執行緒作業(5)  Thread19 準備結束執行 11:41:01.8760643
要求執行緒作業(6)  Thread20 準備結束執行 11:41:01.8770956
要求執行緒作業(7)  Thread21 準備結束執行 11:41:01.8771002
要求執行緒作業(8)  Thread22 準備結束執行 11:41:01.8780994
要求執行緒作業(9)  Thread23 準備結束執行 11:41:02.8740839
要求執行緒作業(10)  Thread24 準備結束執行 11:41:03.8750737
要求執行緒作業(11)  Thread25 準備結束執行 11:41:04.8760519


休息 30 秒,等候執行緒集區清空新建立的執行緒


第 4次,產生 11 執行緒請求
Thread25 已經從執行緒集區取得該執行緒
Thread26 已經從執行緒集區取得該執行緒


等候取得所有執行緒都從執行緒集區取得...
Thread27 已經從執行緒集區取得該執行緒
Thread29 已經從執行緒集區取得該執行緒
Thread28 已經從執行緒集區取得該執行緒
Thread30 已經從執行緒集區取得該執行緒
Thread31 已經從執行緒集區取得該執行緒
Thread32 已經從執行緒集區取得該執行緒
準備把取得的執行緒歸還給執行緒集區...
Thread30 已經歸還給執行緒集區
Thread26 已經歸還給執行緒集區
Thread27 已經歸還給執行緒集區
Thread29 已經歸還給執行緒集區
Thread31 已經歸還給執行緒集區
Thread28 已經歸還給執行緒集區
Thread32 已經歸還給執行緒集區
Thread25 已經歸還給執行緒集區


對於預先配置執行緒將休息 4000 ms, 對於新產生的執行緒將休息 4000 ms

要求執行緒作業(1) ** Thread25 從執行緒集區取得該執行緒 11:41:36.8911002
要求執行緒作業(2) ** Thread32 從執行緒集區取得該執行緒 11:41:36.8911329
要求執行緒作業(3) ** Thread28 從執行緒集區取得該執行緒 11:41:36.8911674
要求執行緒作業(4) ** Thread31 從執行緒集區取得該執行緒 11:41:36.8911972
要求執行緒作業(5) ** Thread27 從執行緒集區取得該執行緒 11:41:36.8912271
要求執行緒作業(6) ** Thread26 從執行緒集區取得該執行緒 11:41:36.8912570
要求執行緒作業(8) ** Thread30 從執行緒集區取得該執行緒 11:41:36.8914003
要求執行緒作業(7) ** Thread29 從執行緒集區取得該執行緒 11:41:36.8914003
要求執行緒作業(9) Thread33 是執行緒集區額外新建立的執行緒 11:41:36.8928622
要求執行緒作業(10) Thread34 是執行緒集區額外新建立的執行緒 11:41:37.8943194
要求執行緒作業(11) Thread35 是執行緒集區額外新建立的執行緒 11:41:38.8943503
要求執行緒作業(1) ** Thread25 準備結束執行 11:41:40.8939245
要求執行緒作業(2) ** Thread32 準備結束執行 11:41:40.8939246
要求執行緒作業(3) ** Thread28 準備結束執行 11:41:40.8949295
要求執行緒作業(4) ** Thread31 準備結束執行 11:41:40.8959405
要求執行緒作業(5) ** Thread27 準備結束執行 11:41:40.8959396
要求執行緒作業(6) ** Thread26 準備結束執行 11:41:40.8969167
要求執行緒作業(8) ** Thread30 準備結束執行 11:41:40.8969237
要求執行緒作業(7) ** Thread29 準備結束執行 11:41:40.8979483
要求執行緒作業(9)  Thread33 準備結束執行 11:41:40.8989145
要求執行緒作業(10)  Thread34 準備結束執行 11:41:41.8949817
要求執行緒作業(11)  Thread35 準備結束執行 11:41:42.8949989


Press any key for continuing...

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [ThreadPool Class] 的使用方式





2019年8月27日 星期二

使用 Task.Run 中使用 CancellationTokenSource 與 CancellationToken 的注意事項

使用 Task.Run 中使用 CancellationTokenSource 與 CancellationToken 的注意事項

CancellationTokenSource Class 所建立的物件,可以取得一個 CancellationTokenSource.Token ,用於 向 CancellationToken 發出訊號,表示應該將它取消,也就是說,當要建立一個非同步應用工作的時候,將會在非同步工作內使用輪詢的方式來檢查,是否有發送出一個 取消執行 的訊號出來,一旦發現到這個訊號,對於非同步工作的程式碼可以選擇:(1) 正常結束與離開這個非同步處理作業 或者是 (2) 使用 CancellationToken.ThrowIfCancellationRequested 拋出一個 OperationCanceledException 例外異常出來,異常終止這個非同步處理作業;當然,不論使用哪種方式,當要終止非同步作業的時候,記得要將已經取得的資源歸還給系統與維持整個處理程序的資料一致性,避免整個處理程序處於不穩定的狀態。

了解更多關於 [Task Class] 的使用方式


了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式



在這篇文章中將會使用 Task.Run 來建立一個非同步工作 (如下面程式碼所列出),不過, Task.Run 方法,有這個多載,需要傳入一個委派方法與一個 CancellationToken 物件;如同前面的說明,在這個委派方法內,需要隨時檢查是否已經發出 取消訊號,這樣可以正常結束委派方法的執行,此時,在委派方法內需要能夠得到 CancellationToken 物件,對於這個 public static System.Threading.Tasks.Task Run (Action action, System.Threading.CancellationToken cancellationToken); 多載方法所傳入的 CancellationToken 物件,在非同步工作的委派方法內,是無法讀取到的,那麼,為什麼已經有在委派方法內檢查了取消權杖的訊號發送狀態,還需要有這樣的多載方法,再次傳入一個取消權杖物件到 Task.Run 方法內呢?
在這篇文章所提到的專案原始碼,可以從 GitHub 下載
C Sharp / C#
CancellationTokenSource cancellationToken = new CancellationTokenSource();
CancellationToken token = cancellationToken.Token;

// 狀況1 : 在非同步工作啟動後一秒種,發出取消執行訊號
cancellationToken.Cancel();

var MyTask = Task.Run(() =>
{
    Console.WriteLine("正在啟動非同步工作");
    Thread.Sleep(5000);
    if (token.IsCancellationRequested)
    {
        Console.WriteLine("非同步工作已經取消了");
    }
    Console.WriteLine("非同步工作結束了");
}, token);

// 狀況2 : 在非同步工作啟動後一秒種,發出取消執行訊號
//Thread.Sleep(1000);
// 狀況3 : 在非同步工作啟動後一秒種,發出取消執行訊號
//cancellationToken.Cancel();

Console.WriteLine("按下任一按鍵,檢查工作狀態");
Console.ReadKey();

Console.WriteLine($"工作狀態 {MyTask.Status}");

Console.WriteLine("Press any key for continuing...");
Console.ReadKey();

情境一:在啟動非同步工作前,就已經發出取消訊號

  • 請將狀況1 底下的 cancellationToken.Cancel(); 解除註解
  • 請將狀況2, 狀況3 後的程式碼註解起來,這包括了 Thread.Sleep(1000); 與 cancellationToken.Cancel();
  • 現在,請執行這個專案,底下是執行結果輸出內容
按下任一按鍵,檢查工作狀態
 工作狀態 Canceled
Press any key for continuing...
從執行結果可以看出,因為在非同步工作建立之前,就已經執行 cancellationToken.Cancel() 方法,發出了取消訊號,而在 Task.Run 的方法引數也有傳遞 CancellationToken 進去,因此,這個非同步工作所指定的委派方法尚未執行前,就已經取消了此非同步工作。
那麼,為什麼要有這樣的設計呢?這是因為要啟動與執行一個非同步工作是相當耗費計算機執行成本的,所以,若在執行非同步工作前,就已經發出取消訊號,最好的做法就是不要去執行非同步工作指定的委派方法,就直接取消非同步工作的執行,要不然,就需要開始執行非同步委派方法,等到該委派方法內才能夠檢查是否已經發出取消訊號了。

情境二:Task.Run 不要傳送 CancellationToken

  • 維持上述的程式碼,請將 Task.Run 方法內的第二個引數 , token,暫時移除
  • 請再度執行該專案
按下任一按鍵,檢查工作狀態
正在啟動非同步工作
 工作狀態 Running
Press any key for continuing...
非同步工作已經取消了
非同步工作結束了
從上述執行結果內容可以看出,雖然一開就已經送出取消訊號,還是會進入到非同步委派方法內來執行,所以會看到 正在啟動非同步工作 字串顯示在螢幕上,緊接著休息五秒鐘,才會檢查是否發送出取消訊號。這與狀況一的執行結果絕然不同,因此,若取消權杖在非同步工作啟動與執行前,就已經發送出訊號之後,最好是在 Task.Run 方法內,將取消權杖傳送進去。

情境三:執行 Task.Run 之後,立即送出權杖取消訊號

  • 請將狀態2 底下的 Thread.Sleep(1000); 程式碼註解
  • 請再度將 Task.Run 方法內,加入第二個引數 , token
  • 請將狀態3 後的程式碼解除註解,也就是 cancellationToken.Cancel();
  • 請再度執行該專案
按下任一按鍵,檢查工作狀態
 工作狀態 Canceled
Press any key for continuing...
在這裡,發送取消訊號,也就是 cancellationToken.Cancel(); 敘述是在 Task.Run 之後才執行,可是,不要以為就會直接進入到非同步工作的委派方法內,從上面的執行結果內容,可以看出,在 Task.Run 這個方法尚未啟動與執行非同步委派方法前,就已經偵測到權杖取消訊號了,因此,直接取消了此非同步作業。
會有這樣的結果,那是因為要啟動與執行非同步工作,需要一些時間,而 cancellationToken.Cancel(); 敘述緊接著 Task.Run 之後就直接執行,所以,這個敘述會先執行,之後非同步工作的運作環境準備好之後,需要開始執行時,就會檢查取消權杖訊號是否已經發送出,此時,就會看到這樣的執行結果輸出內容了。
+

情境四:執行 Task.Run 之後,等候 1 秒鐘,送出權杖取消訊號

  • 請將狀態2 底下的 Thread.Sleep(1000); 解除註解
  • 請將狀態3 底下的 cancellationToken.Cancel(); 解除註解
  • 請再度執行該專案
正在啟動非同步工作
按下任一按鍵,檢查工作狀態
非同步工作已經取消了
非同步工作結束了
 工作狀態 RanToCompletion
Press any key for continuing...
現在,非同步工作已經正常執行起來,接著才會發出取消權杖訊號,所以,當非同步工作委派方法執行之後的五秒鐘,在檢查取消權杖狀態的時候,就會終止非同步工作的作業

了解更多關於 [Task Class] 的使用方式


了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式