2019年9月4日 星期三

C# 非同步工作 Task 的建立、執行、傳遞參數、結束、回傳值、取消、例外異常 之使用教學

C# 非同步工作 Task 的建立、執行、傳遞參數、結束、回傳值、取消、例外異常 之使用教學

上一篇文章 C# 執行緒 Thread / ThreadPool 的建立、執行、傳遞參數、結束、回傳值、取消、例外異常 之使用教學 中,實際學習如何使用執行緒與執行緒集區來建立或者取得一個執行緒,接著,讓兩個委派方法同時進行執行,形成一個非同步的應用程式;在這篇文章中,將會使用 .NET Framework 4.0 推出的 工作平行程式庫 (TPL Task Parallel Library) ,透過 TPL 程式庫提供的 Task 類別,其已經將執行緒抽象化到 Task 類別內,因此,想要設計一個非同步的計算的程式,可以不再需要自己去嘗試建立或者取得一個執行緒,對執行緒進行相關操作,就可以完成一個非同步作業的程式設計工作;而且,在 TPL 所提供的 Task 類別內,已經將之前直接使用執行緒所遇到的相關困境,進行了重新設計與規劃,簡化了許多非同步程式設計上的問題。

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


首先將會先建立兩個方法,這兩個方法分別是執行迴圈 800 與 500 次,分別在螢幕上輸出 * 與 呼叫端所傳入的 - 這兩個符號,程式碼如下所示,其中在 MyMethod2 這個方法中,將會接收一個型別為 object 的 message 參數,這裡要設定參數型別為 object,這是因為等下在建立Task物件的時候,對於 Task 的建構函式共有兩種方式可以指定其非同步的委派方法,第一種是使用 public Task(Action action);,這裡要指定一個沒有回傳值與沒有任何參數的委派方法 ,第二種則是要使用 public Task(Action<object> action, object state);,要傳入一個委派方法,其參數型別必須是 object 型別。最後會使用 Console.Write(message.ToString()); 敘述將這個參數物件值顯示在螢幕上。
C Sharp / C#
static void MyMethod1()
{
    for (int i = 0; i < 800; i++)
    {
        Console.Write("*");
    }
}
static void MyMethod2(object message)
{
    for (int i = 0; i < 500; i++)
    {
        Console.Write(message.ToString());
    }
}
接著,將會使用這兩個方法,開始說明與進行教學,讓大家了解到如何在 C# 程式語言內,操作與使用 Task 這個物件,進行非同步程式設計。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

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

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

建立 Task 工作 - 使用 Task 類別來建立一個非同步工作用法

與 Thread 執行緒類別相同,想要建立一個非同步工作物件,至少需要指定一個委派方法,這個委派方法可以是指定一個沒有回傳值與沒有任何參數的委派方法,其函式簽章必須為 public Task(Action action);,另外一種委派方法則是一樣沒有回傳值,但是可以接受一個型別為 object 的參數,其函式簽章為 public Task(Action<object> action, object state);,若使用這種方式,需要在建立非同步工作物件的時候,就要指定要傳入的引數物件。
在這個範例中,將會建立兩個 Task 物件,一個使用 MyMethod1 (沒有回傳值與沒有參數),另外一個使用 MyMethod2 (沒有回傳值與有一個參數)。
不論是使用 Thread 類別或者 Task 類別所建立起來的物件,這個物件裡面的委派方法在建立起物件的時候,是不會直接執行的,需要呼叫 Start() 方法來驅使委派方法以非同步的方式來執行;Task.Start() 與 Thread.Start() 的不同在於,若有引數需要傳入到委派方法,對於 Task 物件是要在呼叫建構函式的時候傳入,對於 Thread 物件,則是要使用 Start() 方法來傳入。
最後,由於建立與啟動兩個非同步工作物件,若沒有適當的設計,程式設計師是無法知道這兩個工作何時結束,因為他們是採用非同步方式來運行的,所以,在這裡,會先暫停主執行緒 2 秒種,等候這兩個非同步工作執行完成,才會繼續主執行緒的執行。
C Sharp / C#
private static void 建立工作_用Task類別來建立一個非同步工作用法()
{
    Task task1 = new Task(MyMethod1);
    Task task2 = new Task(MyMethod2, "-");
    task1.Start();
    task2.Start();

    Thread.Sleep(2000);
}
底下是執行輸出結果,從執行結果可以看出,這兩個非同步的工作,正在使用並行的方式來執行,因此,會看到 * 與 - 交錯的顯示在螢幕上,所以,可以驗證 Task 類別所產生的物件,是可以使用非同步方式的執行所指定的委派程式碼。
對於 Task 建立起來的非同步工作,有另外一個 Task 類別,可以輕鬆地建立起有回傳值的非同步工作,關於這個部分,將會在後面來進行解說
***********-----------------------------------------------------------------------------------------------------------------------*************************************************************************************************************************************--------------------------------------------------------------------------------------------------------------------------*******************************************************************************************************************************************************-----------------------------------------------------------******************************************************************************************************************************************************************************--------------------------------------------------------------------------------------------------------------------------------------**************************************************************************************************************************************------------------------------------------------------------------*****************************************************************************************************************************************************************************************************

透過 Task.Factory 工廠方法建立一個非同步工作與傳遞參數用法

在 TPL 中,也提供了一個工廠方法, Task.Factory,可以透過這個工廠方法直接產生出一個非同步工作,這與執行緒集區的用法有些差異
  • 透過 Task.Factory 工廠方法所取得的工作物件,預設使用 執行緒集區 ThreadPool 來取得一個新的執行緒作為非同步運算的物件
  • 該工廠方法會回傳一個非同步工作物件,因此,後續可以透過該 工作 物件進行後續相關的操作,例如,等候,取得非同步結果值,這部分與執行緒集區不同,因為透過執行緒集區是無法回傳當時執行緒的物件。
透過該工廠方法提供的 StartNew 方法,可以直接指定一個沒有參數的委派方法,在這裡指的是 MyMethod1,或者可以指定有一個參數的委派方法,這裡指的是 MyMethod2,當使用這樣的方式,需要在 StartNew 方法之後的第二個參數,將要傳遞到委派方法內的參數物件,透過 StartNew 方法傳送過去。
當透過工廠方法取得的一個非同步工作,這個非同步工作就已經正在非同步執行中,無須再度呼叫 Start 方法來啟動這個非同步工作。
C Sharp / C#
private static void 透過Task_Factory工廠方法建立一個非同步工作與傳遞參數的用法()
{
    Task task1 = Task.Factory.StartNew(MyMethod1);
    Task task2 = Task.Factory.StartNew(MyMethod2, "-");

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

透過 Task.Run 工廠方法建立一個非同步工作與傳遞參數用法

在 .NET Framework 4.5 之後,提供了一個新的方法,可以更加方便地建立起一個非同步工作物件,那就是 Task.Run 方法,這也是微軟建議使用建立一個 Task 物件的方法,微軟的說法為: Run方法提供一組多載, 可讓您輕鬆地使用預設值來啟動工作。 這是多載的StartNew輕量替代方式
不過,當使用 Task.Run 方法的時候,該方法沒有提供相關多載方法,提供傳遞參數到委派方法之內,因此,想要傳遞參數到非同步的委派方法內,可以在呼叫 Task.Run 方法的時候,使用 Lambda 提供一個匿名的委派方法,在該 Lambda 委派方法內,來呼叫值實際要傳遞的引數,在這個範例碼中 Task task2 = Task.Run(() => MyMethod2("-")); ,使用了這樣的用法,在 Lambda 表示式內,直接將引數用於呼叫的委派方法上;當然,透過這樣的用法,可以傳遞一個以上的參數到委派方法內。
當透過 Task.Run 方法取得的非同步工作物件,如同使用工廠方法取得的一個非同步工作,這個非同步工作就已經正在非同步執行中,無須再度呼叫 Start 方法來啟動這個非同步工作。
C Sharp / C#
private static void 透過Task_Run方法建立一個非同步工作與傳遞參數的用法()
{
    Task task1 = Task.Run(MyMethod1);
    Task task2 = Task.Run(() => MyMethod2("-"));

    Thread.Sleep(2000);
}
底下是執行輸出結果,在這裡,使用 Task.Run 方法,一樣會取得一個非同步工作物件。
*******----------------------------------------------------------------------------------------------------------------------------------------------------------------------------************************************************************************************************************************************************---------------------------------------------------------------------------------------------------------------------------------------------------------*********************************************************************************************************************************-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************

當使用工作執行個體,如何等待該執行完成

任何的工作物件都會有個 Wait() 方法,可以使用封鎖式的等待方式來等待非同步工作的執行完畢,因此,在這裡可以使用 task1.Wait();task2.Wait(); 這樣的敘述來等待這兩個非同步工作完成;不過,若要同時等候所有的工作物件都已經完成了,可以使用 Task.WhenAll() 這個靜態方法,透過這個靜態方法,把要同時等待的工作物件逐一傳入到這個方法內,或者使用陣列的方式將其傳入進去,這樣,就可以透過 Task.WhenAll() 方法回傳另外一個工作物件,此時,僅需要直接使用該工作物件的 Wait() 方法來等候這些非同步工作的完成即可,底下的程式碼就是要完成這樣的需求。
C Sharp / C#
private static void 當使用工作執行個體_如何等待該執行完成()
{
    Task task1 = Task.Run(MyMethod1);
    Task task2 = Task.Run(() => MyMethod2("-"));

    Task.WhenAll(task1, task2).Wait();
}
底下是執行輸出結果,你將會發現到 請按任意鍵繼續 . . . 這個文字很快就會出現了
*********--------------------------------------------------------------------------------------***************************************************************************************************************************************************************************************************---------------------------------------------------------------------------------------------------------------------*******************************************************************************************************************************--------------------------------------------------------------------------------------------------------------------------------------*****************************************************************************************************************************************************----------------------------------------------------------------------------------------------------------------------------------------------------*******************************************************************************************************************************************************************---------------*************************************************************************************************************************************************************

如何取得非同步工作執行完成之後的執行結果回傳值

不像在使用執行緒來設計非同步應用的時候會很麻煩,在使用非同步工作來設計出一個非同步應用需求,該非同步委派方法可以接受回傳任何型別的物件回來,這樣的功能是透過 Task 這個泛型類別來做到,而之前所使用到的 Task 類別,則一定只接受回傳值為空值的委派方法,也就是 void。
為了要能夠學習如何使用 Task 物件來回傳非同步執行結果,這裡需要把 MyMethod1 & MyMethod2 這兩個方法重新設計,因為他們都是沒有回傳值的函數,在這裡將會設計出兩個方法, MyMethod11 & MyMethod21 ;這兩個方法分別會回傳字串與整數,其這兩個方法的定義如下程式碼。
C Sharp / C#
static string MyMethod11()
{
    for (int i = 0; i < 800; i++)
    {
        Console.Write("*");
    }
    return "loop is 800";
}
static int MyMethod21(object message)
{
    for (int i = 0; i < 500; i++)
    {
        Console.Write(message.ToString());
    }
    return 500;
}
在用戶端這裡,需要使用 Task.Run() 這個泛型方法來取得一個工作物件,而這樣的方法將會回傳一個 Task的工作物件,其中 T 這個型別參數將會表示該非同步工作的回傳結果型別。例如, MyMethod11 方法將會回傳 string 字串型別,所以,在這裡需要使用 Task<string> task1 = Task.Run<string>(MyMethod11); 這樣的敘述來建立起一個具有回傳 string 結果的工作物件;當然,也可以使用簡化方式來寫出同樣需求的程式碼,例如 var task2 = Task.Run(() => MyMethod21("-"));,編譯器會幫助我們推論出這是一個 Task<int> 型別的工作物件,該非同步工作將會回傳 int 數值。
C Sharp / C#
private static void 如何取得非同步工作執行完成之後的執行結果回傳值()
{
    Task<string> task1 = Task.Run<string>(MyMethod11);
    Task<int> task2 = Task.Run<int>(() => MyMethod21("-"));
    // 也可以使用底下寫法,讓編譯器自動推斷回傳型別
    //var task1 = Task.Run(MyMethod11);
    //var task2 = Task.Run(() => MyMethod21("-"));

    Task.WhenAll(task1, task2).Wait();

    Console.WriteLine();
    Console.WriteLine($"task1 的執行結果為 {task1.Result}");
    Console.WriteLine($"task1 的執行結果為 {task2.Result}");
}
底下是這個範例程式碼執行後的螢幕輸出內容。
********-------------------------------------------------------------------------------------------------------------------------------*******************************************************************************************************************************************************************************-----------------***********************************************************************************---------------------****************************************************************************************************************************************************************************---------------------------------------------------------------------------------------------------------------------------------------------------***************************************************************************************************************************************************************----------------------------------------------------------------------------------------------------------------------------------------------------------------------------*************************************************************************************************************************************----------------**********************************************************************
task1 的執行結果為 loop is 800
task1 的執行結果為 500

當使用工作執行個體,如何取消正在執行的非同步工作

當想要取消一個正在執行的非同步工作,通常將會使用 CancellationTokenSource,相信很多人都會使用 Thread.Abort() 的 CancellationTokenSource.Token 這個屬性,把這個物件傳遞到非同步方法內,如此,在非同步方法內將會使用 輪詢 Polling 的方式來檢查是否用戶端有發送出取消請求,若有發生取消請求,可以有兩種選擇,第一種就是正常的結束這個非同步工作,這個方法的好處就是可以進行該非同步工作執行過程中,把之前請求的資源逐一釋放回系統內,避免造成該應用程式的狀態變成不穩定的狀態;第二種方式就是拋出 OperationCanceledException 例外異常,讓這個非同步委派方法立即結束執行,因為有例外異常發生了,不過,這與執行緒會有些不同,當在非同步工作中遇到了例外異常,將不會立即終止整個處理程序,而是將這個例外異常資訊與狀態儲存在工作物件內,在用戶端可以檢查非同步完成結果 (一般來說,非同步工作的結束會有 3 種狀態:正常結束、有例外異常、取消)。
在此先建立兩個非同步工作會用到的委派方法,在這兩個方法內,將會傳入一個 CancellationToken 物件,並且在兩個委派方法內使用輪詢的方式來判斷是否有取消請求發生了。
在 MyMethod12 方法內,將會輪詢檢查取消權杖的 IsCancellationRequested 是否為 True,若是為真,則表示用戶端有發出取消請求,所以,在這裡需要開始清除已經配置的資源,並且正常結束該委派方法的執行,在這樣的狀況下,該工作的最後狀態將會是正常完成,因為非同步委派方法正常結束了。
在 MyMethod22 方法中,將會使用取消權杖的 ThrowIfCancellationRequested() (請記得要經常呼叫這個方法),若使用端沒有請求取消要求,則這個方法不會發生任何問題,不過,若是用戶端有發出取消請求,則當呼叫 ThrowIfCancellationRequested() 方法的時候,將會拋出 OperationCanceledException 例外異常,此時,該工作的狀態將會設定為已經取消了。
C Sharp / C#
static void MyMethod12(CancellationToken token)
{
    while (true)
    {
        Console.Write("*");
        Thread.Sleep(30);
        // 這裡的做法將會是正常結束此非同步工作
        if (token.IsCancellationRequested)
        {
            break;
        }
    }
}
static void MyMethod22(object message, CancellationToken token)
{
    while (true)
    {
        Console.Write(message.ToString());
        Thread.Sleep(30);
        // 這裡的做法將會是立即結束此非同步工作
        token.ThrowIfCancellationRequested();
    }
}
現在,來看看用戶端的程式碼,在這裡需要先產生出一個 CancellationTokenSource 物件,該物件的 Token 屬性將會是一個 CancellationToken 型別,在這裡需要將這個屬性傳送到委派方法內,另外,也需要將這個物件傳入 Task.Run 方法內的第二個參數內。
在這段範例程式碼,將會先暫停主執行緒 1 秒鐘的時間,緊接著使用 CancellationTokenSource.Cancel() 方法,發送出取消請求的訊號,為了要能夠讓非同步工作正常設定工作結束的狀態,在這裡也暫停主執行緒 0.1 秒的時間,最後,將會使用工作物件的 IsCompleted, IsCanceled, IsFaulted 這三個屬性值,看看何者被設定為 True,那個屬性值就是該工作的最後完成狀態。
C Sharp / C#
private static void 當使用工作執行個體_如何取消正在執行的非同步工作()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    Task task1 = Task.Run(()=> MyMethod12(token), token);
    Task task2 = Task.Run(() => MyMethod22("-", token), token);

    Thread.Sleep(1000);

    cts.Cancel();

    Thread.Sleep(100);

    Console.WriteLine();
    Console.WriteLine($"task1 的狀態值為 IsCompleted={task1.IsCompleted}," +
        $"IsCanceled={task1.IsCanceled}, IsFaulted={task1.IsFaulted}");
    Console.WriteLine($"task2 的執行結果為  IsCompleted={task2.IsCompleted}," +
        $"IsCanceled={task2.IsCanceled}, IsFaulted={task2.IsFaulted}"); 
}
底下是執行結果,從執行結果與剛剛所設計的程式碼可以看的出來,當 task1 & task2 都收到了取消請求訊號,task1 將會使用正常結束方式來結束非同步委派方法的執行,所以,可以看到 task1.IsCompleted 的屬性值為 true,表示工作1這個物件是正常結束執行,也就是完成了非同步工作;而在 task2 物件中,因為是使用 ThrowIfCancellationRequested() 來檢查是否發出了取消要求的訊號,所以,當 task2 非同步工作收到了取消訊號之後,並且執行 ThrowIfCancellationRequested() 方法的時候,將會拋出 OperationCanceledException 例外異常,造成工作2異常結束執行,此時,可以從 task2.IsCompleted 與 task2.IsCanceled 屬性值看出該屬性值為 true,表示工作2 是處於取消的工作狀態。
*--*-**-*--*-*-**--**--**--**--**-*-*--**-*-*-*-*-*--**--**--**-*-
task1 的狀態值為 IsCompleted=True,IsCanceled=False, IsFaulted=False
task2 的執行結果為  IsCompleted=True,IsCanceled=True, IsFaulted=False

當工作的委派方法拋出例外異常,會有甚麼問題產生

最後,在來了解當在非同步工作中有發生了例外異常的時候,並不會造成整個處理程序異常終止執行,這點將會與使用執行緒物件來設計非同步作業上有著明顯的不同,也就是改善了非同步程式設計的複雜度使其大為降低了許多。為了要測試非同步委派方法有例外異常拋出,將會重新改寫這兩個委派方法, MyMethod13, MyMethod23 ,如底下所示。
現在,在 MyMethod13 方法中,將會在迴圈索引值為 10 的時候,拋出一個例外異常,造成這個非同步委派方法異常終止執行,這可以從底下的執行結果看出,螢幕上僅有顯示出 10 個 * 星號,而 MyMethod23 方法,將會列印出 500 個 - 字元。
C Sharp / C#
static void MyMethod13()
{
    for (int i = 0; i < 800; i++)
    {
        if(i==10)
        {
            throw new Exception("喔喔,有例外異常拋出了");
        }
        Console.Write("*");
    }
}
static void MyMethod23(object message)
{
    for (int i = 0; i < 500; i++)
    {
        Console.Write(message.ToString());
    }
}
在用戶端這裡,將會使用 Task.Run 方法產生出兩個非同步工作,接著在主執行緒休息 1 秒鐘,等候這兩個非同步工作完成,不過, task1 這個非同步工作很快地就會拋出例外異常,不過,將會看到這不會造成處理程序異常終止,之後,將會從 task1.IsCompleted , task1.IsFaulted 這兩個屬性直接為 true,這表示非同步工作1已經完成執行,並且是因為有例外異常而造成結束的
C Sharp / C#
private static void 當工作的委派方法拋出例外異常_會有甚麼問題產生()
{
    Task task1 = Task.Factory.StartNew(MyMethod13);
    Task task2 = Task.Factory.StartNew(MyMethod23, "-");

    Thread.Sleep(1000);

    Console.WriteLine();
    Console.WriteLine($"task1 的狀態值為 IsCompleted={task1.IsCompleted}," +
        $"IsCanceled={task1.IsCanceled}, IsFaulted={task1.IsFaulted}");
    Console.WriteLine($"task2 的執行結果為  IsCompleted={task2.IsCompleted}," +
        $"IsCanceled={task2.IsCanceled}, IsFaulted={task2.IsFaulted}");
}
這裡是執行結果
------------**********--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
task1 的狀態值為 IsCompleted=True,IsCanceled=False, IsFaulted=True
task2 的執行結果為  IsCompleted=True,IsCanceled=False, IsFaulted=False

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







1 則留言: