本文將介紹 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