Async CTP為我們在單線程實現非同步作業開闢了一條大道,尤其對於SL中的WCF來說讓我們從繁瑣的事件處理中解脫出來,本來我想寫一個SL中使用Socket實現的RPC架構(正在我項目中使用)的系列筆記,不過有朋友提到了應該用Async CTP來規避WCF的非同步方法呼叫帶來的繁瑣,事實上Async CTP在我項目中還不夠靈活(應該是WCF對我的應用程式來說不夠靈活),不過還是先放下那個系列,先看看Async CTP可以做些什麼。
本文不會介紹Async CTP的使用方法,只分析在不改動SL CLR的基礎上編譯器如何根據async和await兩個關鍵字對我們的代碼做出正確的改動以達到單線程非同步作業。
關於單線程非同步可以參考 Programming user interfaces using F# workflows
要理解Async CTP強力推薦此文 Asynchronous Programming in C# using Iterators
先來看看AsyncCtpLibrary_Silverlight.dll中的幾個重要的類,在System.Runtime.CompilerServices命名空間中找出這幾個類AsyncMehodBuilder, AsyncMethodBuilder<TResult>, VoidAsyncMethodBuilder, TaskAWaiter, TaskAwaiter<TResult>,以及System.Threading.Tasks下面的Task, Task<TResult>看到名字基本可以知道是幹啥的了,簡單介紹一下:
AsyncMethodBuilder: 假如async被施加在一個Task對象之前,編譯器使用這個類。
AsyncMethodBuilder<TResult>: 假如async被施加在Task<TResult>之前, 編譯器使用這個類。
VoidAsyncMethodBuilder: async被施加在一個方法聲明之前,編譯器使用這個類。(因此只有傳回值是void的方法能加async關鍵字)
TaskAwaiter: 對應在Task對象之前的await關鍵字。
TaskAwaiter<TResult>:對應在Task<TResult>對象之前的await關鍵字。
Task、Task<TResult>:對應兩個await。
為了實現async,await編譯器將每個被async關鍵字標記的方法編譯為一個方法所在類的一個內嵌類,所有在方法體內出現的變數會被聲明為這個類的field,如果是一個執行個體方法,那麼this所代表的對象也被聲明為一個field。這個類有兩個核心成員:一個int來儲存代碼執行到那一步,暫且叫它step,一個方法來執行真正的動作,暫且叫做NextStep,整個邏輯看起來應該是這樣的:
1: public void NextStep()
2: {
3: switch (step)
4: {
5: case 1:
6: ...
7: step++;
8: break;
9: case 2:
10: ...
11: step++;
12: break;
13: case 3:
14: ....
15: step++;
16: break;
17: .
18: .
19: .
20: }
21: }
是不是覺得和yield return很像?
而在async標記的方法大體是如此,假設方法被編譯為一個命為AsyncMethodClass的類:
1: AsyncMethodClass a = new AsyncMethodClass();
2: a.xxx = xxx;
3: a.yyy = yyy;
4: .
5: .
6: .
7: a.NextStep();
那麼編譯根據什麼來決定一個方法分成幾個塊呢?實際上,編譯器根據一個async方法中出現的await關鍵字來進行分布,假如有一個await,那麼應該有2個Step,假如有2個await,那麼應該有3個Step。每一個Step,應該是以上一個Step中的await的EndWait開始(第一個Step除外),並以下一await的BeginWait結束(最後一個Step除外)。看到這裡就理解了為什麼async只能被標記在無傳回值的函數上,因為NextStep函數必須要是一個無傳回值的Action類型,傳遞給await的BeginWait方法,因此在上述代碼中,在最後的第7行沒有辦法使用理論上的:
1: return a.NextStep();
弄明白了編譯器乾的活之後,最後來看一個執行個體,來自於Async CTP Samples的片段:
1: public async void AsyncIntroSerial()
2: {
3: var client = new WebClient();
4:
5: WriteLinePageTitle(await client.DownloadStringTaskAsync(new Uri("http://www.weather.gov")));
6: WriteLinePageTitle(await client.DownloadStringTaskAsync(new Uri("http://www.weather.gov/climate/")));
7: WriteLinePageTitle(await client.DownloadStringTaskAsync(new Uri("http://www.weather.gov/rss/")));
8: }
經過上面的理論,不使用async和await關鍵字,改寫如下:
1: TaskAwaiter<string> wait1;
2: TaskAwaiter<string> wait2;
3: TaskAwaiter<string> wait3;
4: int step = 0;
5: WebClient wc = new WebClient();
6: private void AsyncMethodWithoutKeywords()
7: {
8: switch (step)
9: {
10: case 0:
11: Task<string> t1 = wc.DownloadStringTaskAsync(new Uri("http://www.weather.gov"));
12: step++;
13: wait1 = t1.GetAwaiter<string>();
14: wait1.BeginAwait(new Action(AsyncMethodWithoutKeywords));
15: break;
16: case 1:
17: WriteLinePageTitle(wait1.EndAwait());
18: Task<string> t2 = wc.DownloadStringTaskAsync(new Uri("http://www.weather.gov/climate/"));
19: step++;
20: wait2 = t2.GetAwaiter<string>();
21: wait2.BeginAwait(new Action(AsyncMethodWithoutKeywords));
22: break;
23: case 2:
24: WriteLinePageTitle(wait2.EndAwait());
25: Task<string> t3 = wc.DownloadStringTaskAsync(new Uri("http://www.weather.gov/rss/"));
26: step++;
27: wait3 = t3.GetAwaiter<string>();
28: wait3.BeginAwait(new Action(AsyncMethodWithoutKeywords));
29: break;
30: case 4:
31: WriteLinePageTitle(wait3.EndAwait());
32: break;
33: }
34: }
可以發現,執行結果和使用async和await一樣。
Async CTP的核心其實是Task的設計,CTP庫為WebClient和WCF實現了Async到Task的工作,對於項目中出現自訂的一些方法則需要自己去定義Task,不過通過自己的方法定義Task,並且使用本文所描述的方法進行封裝(尤其是在原先使用Emit或者使用dynamic關鍵字來做AOP的場合),那麼可以拋開aysnc和await關鍵字,阻止編譯器對代碼的改動,這樣的好處是斷點調試可以正常進行。