[C#] 將事件驅動 (event-driven) 的模式改為可等候的方法 (awaitable method)

本文將介紹 awaitable method 該如何撰寫,甚至取代過去事件驅動的模式

.Net 4.5 新增了 async 與 await 這兩個保留字。若於 method 前加註 async,代表這個方法是可等候的方法 (awaitable method)。至此,已大大地改變了過去須撰寫冗長程式碼的非同步模式。微軟官方,針對將可能會需要耗費大量時間的 API (如檔案讀寫、網路傳輸),也新增了相對應的 awaitable method。

 

本文將介紹 awaitable method 該如何撰寫,甚至取代過去事件驅動的模式

 

首先要介紹 TaskCompletionSource 這個在 awaitable method 開發中相當重要的 class。TaskCompletionSource 在 awaitable method 中,扮演終結者的腳色。當開發者呼叫 TaskCompletionSource.SetResult(result) 或者  TaskCompletionSource.TrySetResult(result) 時,即代表此 awaitable method 已終結。而 TaskCompletionSource 在宣告時,必須先指名 SetResult 中 result 的型別

 

下面為指定 TaskCompletionSource 的 result 為 String 型別

TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();

 

此時若 tcs 嘗試要呼叫 SetResult,則 result 必須為 String 型別

String result = "result";
tcs.SetResult(result);

 

 

以下為利用 TaskCompletionSource 撰寫 awaitable method 的範例

public async Task<String> MyAwaitableMethod()
{
    TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
    tcs.SetResult("result");
    return await tcs.Task;
}

 

 

上述範例並不能看出 TaskCompletionSource 真正的作用

TaskCompletionSource 真正強大的地方在於,它可以讓開發者隨心所欲地控制 method 結束的時機 (SetResult) 

下述範例,當我呼叫 MyAwaitableMethod2 後,將會開啟一個新的 Task,並且在該 Task 中停留 3 秒後,才結束 MyAwaitableMethod2

public async Task<String> MyAwaitableMethod2()
{
    TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
    await Task.Run(async () =>
    {
        await Task.Delay(TimeSpan.FromSeconds(3));
        tcs.SetResult("result");
    });
    return await tcs.Task;
}

這樣撰寫的好處是什麼?

它讓非同步的流程變得更清晰,更簡潔

從 MyAwaitableMethod2 短短幾行中,便可一目了然地知道,非同步的結束點是在停留 3 秒之後

 

 

將過去的 event-driven 透過 TaskCompletionSource 與 lambda 表示式來改寫成 awaitable method

此處以 WebClient 為範例

public async Task<String> MyAwaitableMethod3()
{
    TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();

    WebClient clinet = new WebClient();
    clinet.DownloadStringCompleted += (obj, args) =>
    {
        tcs.TrySetResult(args.Result);
    };

    clinet.DownloadStringAsync(new Uri("http://www.google.com.tw", UriKind.Absolute));

    return await tcs.Task;
}

 

 

將  event-driven 改寫成 awaitable method,需特別注意到是 Timeout 機制

若是開發者撰寫了一個內含 await  MyAwaitableMethod3(); 程式碼的方法,當 MyAwaitableMethod3 內的 WebClient 因不明原因遲遲未觸發DownloadStringCompleted ,將導致 MyAwaitableMethod3 永遠無法被結束,使得程式可能卡死在這裡。

以下範例將加入 Timeout 機制,讓 MyAwaitableMethod4 在 10 秒後仍未得到 result 時拋出 TimeoutException

public async Task<String> MyAwaitableMethod4()
{
    Int32 timeOutSeconds = 10;
    TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();

    WebClient clinet = new WebClient();
    clinet.DownloadStringCompleted += (obj, args) =>
    {
        tcs.TrySetResult(args.Result);
    };

    Task.Run(async () =>
    {
        await Task.Delay(TimeSpan.FromSeconds(timeOutSeconds));
        tcs.TrySetException(new TimeoutException("WebClient Timeout!!"));
    });

    clinet.DownloadStringAsync(new Uri("http://www.google.com.tw", UriKind.Absolute));

    return await tcs.Task;
}

當然,上述範例你不一定要於 Timeout 時拋出 TimeoutException

您也可以使用 TrySetResult,並將 result 設為 null 來代表 MyAwaitableMethod4 已 Timeout