將事件驅動 (event-driven) 的模式改為可等候的方法 (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 型別

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

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

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

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

  1. public async Task<String> MyAwaitableMethod()
  2. {
  3. TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
  4. tcs.SetResult("result");
  5. return await tcs.Task;
  6. }

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

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

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

  1. public async Task<String> MyAwaitableMethod2()
  2. {
  3. TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
  4. await Task.Run(async () =>
  5. {
  6. await Task.Delay(TimeSpan.FromSeconds(3));
  7. tcs.SetResult("result");
  8. });
  9. return await tcs.Task;
  10. }

這樣撰寫的好處是什麼?

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

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

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

此處以 WebClient 為範例

  1. public async Task<String> MyAwaitableMethod3()
  2. {
  3. TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
  4. WebClient clinet = new WebClient();
  5. clinet.DownloadStringCompleted += (obj, args) =>
  6. {
  7. tcs.TrySetResult(args.Result);
  8. };
  9. clinet.DownloadStringAsync(new Uri("http://www.google.com.tw", UriKind.Absolute));
  10. return await tcs.Task;
  11. }

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

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

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

  1. public async Task<String> MyAwaitableMethod4()
  2. {
  3. Int32 timeOutSeconds = 10;
  4. TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
  5. WebClient clinet = new WebClient();
  6. clinet.DownloadStringCompleted += (obj, args) =>
  7. {
  8. tcs.TrySetResult(args.Result);
  9. };
  10. Task.Run(async () =>
  11. {
  12. await Task.Delay(TimeSpan.FromSeconds(timeOutSeconds));
  13. tcs.TrySetException(new TimeoutException("WebClient Timeout!!"));
  14. });
  15. clinet.DownloadStringAsync(new Uri("http://www.google.com.tw", UriKind.Absolute));
  16. return await tcs.Task;
  17. }

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

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

轉自:http://www.dotblogs.com.tw/renewalwu/archive/2014/07/27/eventdriven2awaitablemethod.aspx