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] 的使用方式





沒有留言:

張貼留言