2019年9月9日 星期一

C# 當要呼叫多個連續非同步作業的各種不同設計方式

C# 當要呼叫多個連續非同步作業的各種不同設計方式

當要進行非同步程式設計的時候,尤其是要採用 TPL / TAP 方式來進行程式碼設計,將會享受到 Task 類別所帶來的許多好處,因為,Task 類別已經將許多非同步程式設計的需求抽象化了,並且可以使用 Task 類別進行非同步作業的設計。在這篇文章中將要討論的是:當有兩個以上的非同步作業需要處理,例如 非同步作業A / 非同步作業B,而這裡需要先完成 非同步作業A 之後,才能夠繼續完成 非同步作業B;這個時候,將會有多種設計方式選擇可以完成這樣的需求,因此,在這裡將會來描述這些不同多個非同步作業的設計方式。

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



對於整個測試環境的模擬將會是:非同步作業A 將會需要從遠端 Web API 上取得回傳結果內容,接著,非同步作業B 需要進一步地把資料寫入到本機磁碟目錄下,因此,可以知道需要等非同步作業A完成之後,才能夠繼續非同步作業B 的運行,而不是同時將 非同步作業A 與 非同步作業B 一起執行,這樣會造成兩個非同步作業沒有同步。
另外,為了要模擬出呼叫 Web API & 寫入檔案的動作,都需要使用非同步作業的方式,因此,請使用 .NET Core 建立一個 .NET Core Console 類型的專案,填入這裡的程式碼範例,實際體驗執行看看。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載
這篇文章所用的程式碼範例,將會在這篇文章的最後面會列出來。

使用工作 ContinueWith

首先要看到的是使用 Task.ContinueWith 方法來將兩個接續的非同步工作連結在一起,這裡寫法的特色:使用一行敘述 (Task 支援 fluent API 寫法),就可以完成所有設計,這似乎是很吸引人的一個要素,不過,來看看要怎麼做到呢?
C Sharp / C#
private static void 使用工作ContinueWith()
{
    string url = "https://lobworkshop.azurewebsites.net/api/RemoteSource/AddASync/15/43/3000";
    var myTask = new HttpClient().
        GetStringAsync(url).ContinueWith(task1 =>
        {
            string content = task1.Result;
            File.WriteAllTextAsync("MyFileContinueWith.txt", content).
            ContinueWith(task2 =>
            {
                if (task2.Status == TaskStatus.RanToCompletion)
                {
                    Console.WriteLine("已經成功下載內容並且寫入到檔案內");
                }
                else
                {
                    Console.WriteLine("寫入檔案發生了問題");
                }
            });
        });

    Thread.Sleep(4000);
}
從上面的程式碼可以看的出來
  • 首先建立一個 HttpClient 物件
  • 接著呼叫 HttpClient 執行個體的 GetStringAsync() 方法,這個方法將會回傳一個 Task<string> 的物件
  • 因此,可以接續這個 Task<string> 物件來使用 ContinueWith 方法,設定這個非同步工作的 callback 回呼事件,也就是設定當這個非同步工作完成之後 (對於非同步工作的完成,將可能會有不同的最後結果狀態,例如:也許是正常完成且有得到結果,或者是執行非同步作業過程中,有拋出例外異常,造成沒有完成這個非同步作業,又或者是這個非同步作業被呼叫端指定要取消該非同步作業的執行),將會來指定所要執行的一個委派方法。
  • 對於 ContinueWith 的委派方法,將會需要有一個 Task<string> 的參數的函式簽章,因此,在這裡使用 Lambda 匿名委派方法來設計,透過傳入的 task1 引數,可以得知上一個非同步工作的執行結果,例如 可以使用 Task.Status 屬性或者是 Task.IsCompleted 、 Task.IsFaulted 、 Task.IsCanceled 。
  • 為了簡化練習,這裡將不做呼叫遠端 Web API 執行結果狀態的檢查,而是繼續使用 File.WriteAllTextAsync() 方法來將資料寫入到本機檔案內
  • 而這個方法將會回傳一個 Task 物件,有了這個物件,就可以使用 ContinueWith 方法來指定這個非同步作業的 callback 委派事件的訂閱
  • 在 File.WriteAllTextAsync 非同步作業的 ContinueWith 委派方法內,將會使用傳入的 task2 引數物件,檢查寫入檔案的結果是否已經正常且成功結束了,若有,會顯示一段文字來說明這個狀態,否則,將會顯示出一段錯誤訊息
  • 整個單一 C# 表示式將會得到一個 myTask 物件,在這裡可以等候這個非同步工作完成或者像是在這裡,故意讓主執行緒休息 5 秒鐘,讓非同步作業完成執行。
透過 ContinueWith 是可以完成這樣的連鎖接續非同步作業的設計,因為採用 callback 技術,將會得到一個好處,這裡僅需要使用一行敘述就可以完成這樣的設計,當然,這也是一個缺點,因為整個程式碼將會變成很難閱讀與維護,整個接續非同步作業的程式碼都完全綁定在一起,是想,若有一個需求是要先完成非同步作業A,接著要完成非同步作業B,然後又要完成非同步作業C,你便可以想像到這個一個 C# 敘述將會變成多麼的複雜,這還不包括每次非同步作業完成之後,還需要確認非同步作業的完成狀態是甚麼,依據這些不同狀態來進行相對應的處理機制的程式碼設計。

同步封鎖等待非同步作業完成

當然,對於進行程式碼設計的時候,若採用同步程式設計的方式,所撰寫出來的程式碼將會具有比較好的閱讀性,因為,程式碼式一行接著一行來執行,不過,想要使用同步程式設計方式,設計出具有非同步執行能力的程式碼,透過 Task 類別所提供的功能是可以做到的;這樣太完美了,設計出具有非同步執行能力的程式碼,但是,採用同步程式碼撰寫風格,便可以滿足需求與撰寫出比較容易閱讀與好維護的程式碼。
現在來看看使用同步封鎖的方式,進行這樣的設計方式。
在 同步封鎖等待非同步作業完成() 方法內,將會使用同步封鎖等待的方式,等待非同步作業完成,然後才要接續接下來的程式碼執行,整段程式碼將會以同步程式設計的方式來進行撰寫,不過,當要進行執行非同步作業的時候,對於呼叫端 Client 將會使用封鎖本身執行緒的方式(該 Client 端執行緒在此時無法執行任何程式碼),一直等候到非同步作業完成。
同樣的呼叫 Web API 與 將取得結果寫入到檔案的需求,使用底下的方式來設計,是不是程式碼變得容易閱讀與除錯了呢?
不過,這樣的設計方式還存在一個缺點,那就是當要等待非同步執行完成的時候,本身執行緒將會被封鎖住,無法執行其他的工作;接下來的第三種方式,將會使用 C# 5.0 提供的 async 修飾詞 與 await 運算子來解決此一問題。
C Sharp / C#
private static void 同步封鎖等待非同步作業完成()
{
    string url = "https://lobworkshop.azurewebsites.net/api/RemoteSource/AddASync/15/43/3000";
    string content = new HttpClient().GetStringAsync(url).Result;
    var task2 = File.WriteAllTextAsync("同步封鎖等待完成.txt", content);
    task2.Wait();
    if (task2.Status == TaskStatus.RanToCompletion)
    {
        Console.WriteLine("已經成功下載內容並且寫入到檔案內");
    }
    else
    {
        Console.WriteLine("寫入檔案發生了問題");
    }
}

await 呼叫非同步方法

第三種設計作法將要改善會造成封鎖呼叫端執行緒的問題,此時,你的開發環境需要使用 C# 5.0 / .NET Framework 4.5 以上的版本才能夠做到,並且設計成為 await呼叫非同步方法Async() 這個方法。
  • 首先,將呼叫遠端 Web API 的程式碼,包裝在一個 非同步方法 ( async Method ) 內,這裡將會設計出 GetRemoteStringAsync() 方法,在此方法內,將會使用 await 運算子 來等待 new HttpClient().GetStringAsync(url) 非同步作業完成;await 運算子 與 wait() 不同在於,後者會造成當前執行緒進入封鎖等候狀態,而前者將會因為非同步作業尚未完成,而會先記住當前執行狀態內容和執行位置,接著直接返回到呼叫 GetRemoteStringAsync() 方法的呼叫端上,當 new HttpClient().GetStringAsync(url) 非同步工作完成之後,將會透過會還原剛剛提到的執行狀態內容與當時離開的程式執行位置,接續 await 之後的程式碼繼續來執行;這些動作將會由編譯器來產生出相關的程式碼,以便可以做到這樣的描述動作。
  • 因此,當 new HttpClient().GetStringAsync(url) 非同步作業完成之後,透過 await 運算子 將會取得該非同步方法回傳的呼叫 Web API 的字串內容,在此將會儲存到 content 區域變數內
  • 緊接著將將會要將 content 內容寫入到檔案內,在這裡將會使用 await 運算子呼叫 WriteRemoteStringAsync("MyFile.txt", content) 非同步方法,同樣的,若非同步方法尚未執行完成,則會立即返回到呼叫端;一旦非同步方法執行完畢之後,將會回到 await 之後來繼續執行。
對於採用 async 修飾詞 與 await 運算子關鍵字的方式將不會造成程式碼進入封鎖等候的狀態,而且,整個程式碼設計邏輯將都是採用同步程式設計的邏輯來進行,只要適當地加入 async / await ,你的程式碼就具有非同步執行的能力與效果,所以,整體的程式碼將會變成比較好閱讀與維護。
C Sharp / C#
private static async Task await呼叫非同步方法Async()
{
    string content = await GetRemoteStringAsync();
    await WriteRemoteStringAsync("MyFile.txt", content);
}

static async Task<string> GetRemoteStringAsync()
{
    string url = "https://lobworkshop.azurewebsites.net/api/RemoteSource/AddASync/15/43/3000";
    string result = await new HttpClient().GetStringAsync(url);
    return result;
}
static async Task WriteRemoteStringAsync(string filename, string content)
{
    await File.WriteAllTextAsync(filename, content);
}

本篇文章中的所有程式碼

C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("這裡將會展示當要呼叫多個非同步作業的各種不同設計方式");

        // 這裡寫法的特色:使用一行敘述 (Task 支援 fluent API 寫法),就可以完成所有設計
        //使用工作ContinueWith();

        // 這裡寫法的特色:需要使用封鎖式等待非同步作業(可能是非同步工作或者非同步方法)的完成
        同步封鎖等待非同步作業完成();

        // 這裡寫法的特色:使用 async 修飾詞 與 await 運算子 ,採用不會直接封鎖的方式來呼叫非同步作業
        // 重點是可以使用同步設計的風格和方式,設計出具有非同步運作能力的程式碼
        //await呼叫非同步方法()Async.Wait();
    }

    private static void 同步封鎖等待非同步作業完成()
    {
        string url = "https://lobworkshop.azurewebsites.net/api/RemoteSource/AddASync/15/43/3000";
        string content = new HttpClient().GetStringAsync(url).Result;
        var task2 = File.WriteAllTextAsync("同步封鎖等待完成.txt", content);
        task2.Wait();
        if (task2.Status == TaskStatus.RanToCompletion)
        {
            Console.WriteLine("已經成功下載內容並且寫入到檔案內");
        }
        else
        {
            Console.WriteLine("寫入檔案發生了問題");
        }
    }

    private static void 使用工作ContinueWith()
    {
        string url = "https://lobworkshop.azurewebsites.net/api/RemoteSource/AddASync/15/43/3000";
        var myTask = new HttpClient().
            GetStringAsync(url).ContinueWith(task1 =>
            {
                string content = task1.Result;
                File.WriteAllTextAsync("MyFileContinueWith.txt", content).
                ContinueWith(task2 =>
                {
                    if (task2.Status == TaskStatus.RanToCompletion)
                    {
                        Console.WriteLine("已經成功下載內容並且寫入到檔案內");
                    }
                    else
                    {
                        Console.WriteLine("寫入檔案發生了問題");
                    }
                });
            });

        Thread.Sleep(4000);
    }

    private static async Task await呼叫非同步方法Async()
    {
        string content = await GetRemoteStringAsync();
        await WriteRemoteStringAsync("MyFile.txt", content);
    }

    static async Task<string> GetRemoteStringAsync()
    {
        string url = "https://lobworkshop.azurewebsites.net/api/RemoteSource/AddASync/15/43/3000";
        string result = await new HttpClient().GetStringAsync(url);
        return result;
    }
    static async Task WriteRemoteStringAsync(string filename, string content)
    {
        await File.WriteAllTextAsync(filename, content);
    }
}


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





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 進行非同步程式設計] 的使用方式