能夠從兩方面來理解async特性:web
當程序運行到await關鍵字時,發生了兩件事:異步
一個阻塞的方法就像是計算機休眠(S3 sleep)同樣,它佔用不多的資源,本質上它還在運行。async
首先,async方法中的全部本地變量都會被記錄下來:方法參數、定義在做用域內的任何變量、其它變量(好比循環計數)、若是不是static方法的話還有this變量。全部這些變量都被做爲一個object存儲在託管堆上。
其次,C#須要記錄方法目前到達那個await了,可能使用一個數字來表示。
另外,相似下面的大型表達式,須要一個棧來存儲子表達式的返回值:性能
int myNum = await MethodAsync(await myTask, await Method2Async());
最後,await表達式返回的Task也須要存儲。優化
C#會在await時獲取各類上下文,而且在方法繼續時恢復上下文。
最重要的上下文是同步上下文(synchronization context),對於UI應用程序尤爲重要。
調用上下文(CallContext),存儲邏輯線程生命週期內的數據。使用在程序中使用這個上下文是一個糟糕的實踐,雖然它能夠減小方法的參數。這個上下文在異步環境下沒有用,由於方法可能在一個徹底不一樣的線程上恢復。ui
await能夠在標記爲async的方法的大多數位置使用,可是有一些例外:this
會使異常難以定義。spa
//非法代碼:try{ page = await webClient.DownloadStringTaskAsync("http://oreilly.com");}catch (WebException){ page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");}//替代的合法寫法:bool failed = false;try{ page = await webClient.DownloadStringTaskAsync("http://oreilly.com");}catch (WebException){ failed = true;}if (failed){ page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");}
lock是爲了防止在同一時刻不一樣的線程訪問同一個對象。可是由於異步代碼會釋放線程,而後在不肯定的時間以後恢復到可能不一樣的線程,這樣一來在await過程當中維護一個lock就徹底沒有必要了。線程
lock (sync){ // 準備調用異步方法}int myNumber = await MethodAsync();lock (sync){ // 使用異步方法的返回值}
在查詢語句中使用await大多數狀況下是非法的。由於,LINQ會被編譯器編譯器編譯成Lambda表達式。Lambda表達式須要被標記爲async。可是編譯器並不會隱式地標記Lambda表達式爲async。
解決方案是,將LINQ查詢語句寫爲等價的擴展方法調用,此時能夠顯示地標記Lambda爲async。設計
IEnumberable<Task<int>> tasks = myInts .Where(x => x != 9) .Select(async x => await DoSomethingAsync(x) + await DoSomethingElseAsync(x));
unsafe代碼應該保持獨立,它不須要是異步的。await關鍵的編譯會破壞unsaf代碼。
async方法在到達第一個await纔會暫停,可是這不是必定的。有時在到達第一個await時,task已經執行完成了。下面狀況下,Task可能已經完成:
下面從一個簡單的async方法來解釋編譯的過程:
public async Task<int> Method1(){ int foo = 6; await Task.Delay(500); return foo;}
編譯器首先會將async方法替換爲一個存根方法(stub Method)。
public Task<int> Mehtod(){ <Method>d_0 stateMachine = new <Method>d_0() stateMachine.<>_this = this; stateMachine.<>t_builder = AsyncTaskMethodBuilder<int>.Create(); stateMachine.<>_state = -1; stateMachine.t_builder.Start<<Method>d_0>(ref stateMachine); return stateMachine.<>t_builder.Task;}
存根方法的大多數工做就是初始化一個結構體的變量(<Method>d_0
)。這是一個狀態機。存根方法調用Start方法,而後返回一個Task。
這個狀態機選擇使用一個struct而不是class,主要是出於性能方面的考慮(若是異步方法是同步完成的,那麼就無需在堆上分配空間了)。
public struct <Method>d_0{ ... public int <>1_state; //標記執行到第幾個await,-1表示未執行 public int <foo>5_1; //保存原方法中的foo變量值 public MyClass <>4_this; //實例方法,保存this變量,靜態方法無此項 public AsyncTaskMethodBuilder<int> <>t_builder; //狀態機共享邏輯的Helper,與TaskCompleteSource相似,區別是它會優化異步方法,而且是一個struct不是class private object <>t_stack; //用於大型表達式中的await。 private TaskAwaiter <>u_$awaiter2; //臨時存儲,Task完成時幫助通知完成。 ...}
MoveNext方法是狀態機必須的方法,它在第一次運行時和從await繼續運行時被調用。該方法須要進行下面的編譯步驟:
<foo>5_1 = 3;Task t = Task.Delay(500);//await繼續的邏輯代碼return <foo>5_1;
源代碼中的每個返回語句都須要轉換。
<>t_builder.SetResult(<foo>5_1);//設置值return; //MoveNext返回void
生成的中間代碼相似下面的switch語句:
switch(<>1_state){ case -1: //第一次調用時 <foo>5_1 = 3; Task t = Task.Delay(500); //await繼續的邏輯代碼 case 0: //第一個await <>t_builder.SetResult(<foo>5_1); return;}
在Task完成時,須要更新狀態。
switch(<>1_state){ case -1: //第一次調用時 <foo>5_1 = 3; //************** u_&awaiter2 = Task.Delay(500).GetAwaiter(); //await繼續的邏輯代碼 <>1_state = 0; <>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this); return; //************** case 0: //第一個await <>t_builder.SetResult(<foo>5_1); return;}
這個過程當中還包括更復雜的過程,好比獲取同步上下文等等。
switch(<>1_state){ case -1: //第一次調用時 <foo>5_1 = 3; u_&awaiter2 = Task.Delay(500).GetAwaiter(); //await繼續的邏輯代碼 <>1_state = 0; <>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this); return; case 0: //第一個await //************** <>u_$awaiter2.GetResult(); //await返回後,獲取返回值 //************** <>t_builder.SetResult(<foo>5_1); return;}
若是await以前,Task已經完成運行,那麼無需暫停,直接goto:
switch(<>1_state){ case -1: //第一次調用時 <foo>5_1 = 3; //Task t = Task.Delay(500); u_&awaiter2 = Task.Delay(500).GetAwaiter(); //若是同步執行,直接goto,無需站廳代碼 if(<>u_$awaiter2.IsCompleted) { goto case 0; } <>1_state = 0; <>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this); return; case 0: //第一個await <>u_$awaiter2.GetResult(); //await返回後,獲取返回值 <>t_builder.SetResult(<foo>5_1); return;}
若是在async方法運行期間拋出了異常,可是沒有try…catch代碼來處理異常,編譯器生成的代碼會捕獲這個異常,而後設置返回的Task爲faulted。