理解async特性

能夠從兩方面來理解async特性:web

  • 從語言特性上講,它定義了一種行爲。
  • 從編譯上講,它是一個語法糖。

1. 運行到await時,async方法的行爲

1.1 休眠和恢復方法

當程序運行到await關鍵字時,發生了兩件事:異步

  • 運行代碼的線程將會被釋放。從普通方法或同步代碼的角度看,就是方法返回了。
  • 當await的Task完成時,方法將會繼續運行,好像歷來沒有返回過同樣。
    這個過程就像計算機睡眠(S4 sleep)同樣,方法的當前狀態被存入硬盤,而後徹底退出,一點內存資源都不會佔用。

    一個阻塞的方法就像是計算機休眠(S3 sleep)同樣,它佔用不多的資源,本質上它還在運行。async

1.2 記錄方法的狀態

首先,async方法中的全部本地變量都會被記錄下來:方法參數、定義在做用域內的任何變量、其它變量(好比循環計數)、若是不是static方法的話還有this變量。全部這些變量都被做爲一個object存儲在託管堆上。
其次,C#須要記錄方法目前到達那個await了,可能使用一個數字來表示。
另外,相似下面的大型表達式,須要一個棧來存儲子表達式的返回值:性能

 
 
 
 
int myNum = await MethodAsync(await myTask, await Method2Async());

最後,await表達式返回的Task也須要存儲。優化

1.3 獲取上下文

C#會在await時獲取各類上下文,而且在方法繼續時恢復上下文。
最重要的上下文是同步上下文(synchronization context),對於UI應用程序尤爲重要。
調用上下文(CallContext),存儲邏輯線程生命週期內的數據。使用在程序中使用這個上下文是一個糟糕的實踐,雖然它能夠減小方法的參數。這個上下文在異步環境下沒有用,由於方法可能在一個徹底不一樣的線程上恢復。ui

1.4 await不能使用的狀況

await能夠在標記爲async的方法的大多數位置使用,可是有一些例外:this

1.catch和finally代碼塊,

會使異常難以定義。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");}

2.lock代碼塊

lock是爲了防止在同一時刻不一樣的線程訪問同一個對象。可是由於異步代碼會釋放線程,而後在不肯定的時間以後恢復到可能不一樣的線程,這樣一來在await過程當中維護一個lock就徹底沒有必要了。線程

  • 若是須要鎖住的資源並非必須異步的,能夠在await先後顯式地使用兩次lock:
 
 
 
 
lock (sync){ // 準備調用異步方法}int myNumber = await MethodAsync();lock (sync){ // 使用異步方法的返回值}
  • 若是確實須要在異步操做中維護一些lock,那麼很不幸,這很容易形成死鎖。最好考慮從新設計代碼結構。

3.LINQ 查詢語句

在查詢語句中使用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));

4.unsafe代碼塊

unsafe代碼應該保持獨立,它不須要是異步的。await關鍵的編譯會破壞unsaf代碼。

1.5 async方法只有在須要時纔是異步的

async方法在到達第一個await纔會暫停,可是這不是必定的。有時在到達第一個await時,task已經執行完成了。下面狀況下,Task可能已經完成:

  • 建立時就完成了。
  • 從一個沒有執行await的async方法返回的Task。
  • 異步操做確實已經完成了。(可能由於在await以前,線程忙於其它工做)
  • 從一個執行到await的async方法返回,可是這個方法中await的Task也已經完成。(此時,整個異步方法鏈是同步的)

2. async的編譯過程

下面從一個簡單的async方法來解釋編譯的過程:

 
 
 
 
public async Task<int> Method1(){ int foo = 6; await Task.Delay(500); return foo;}

2.1 存根方法

編譯器首先會將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。

2.2 狀態機結構體

這個狀態機選擇使用一個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完成時幫助通知完成。 ...}

2.3 MoveNext方法

MoveNext方法是狀態機必須的方法,它在第一次運行時和從await繼續運行時被調用。該方法須要進行下面的編譯步驟:

1.將原方法拷貝到MoveNext方法:

 
 
 
 
<foo>5_1 = 3;Task t = Task.Delay(500);//await繼續的邏輯代碼return <foo>5_1;

2.轉換完成時的返回值

源代碼中的每個返回語句都須要轉換。

 
 
 
 
<>t_builder.SetResult(<foo>5_1);//設置值return; //MoveNext返回void

3.跳轉到正確的位置

生成的中間代碼相似下面的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;}

4.運行到await時暫停方法

在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;}

這個過程當中還包括更復雜的過程,好比獲取同步上下文等等。

5.await以後繼續運行

 
 
 
 
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;}

6.同步完成

若是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;}

7.捕獲異常

若是在async方法運行期間拋出了異常,可是沒有try…catch代碼來處理異常,編譯器生成的代碼會捕獲這個異常,而後設置返回的Task爲faulted。



相關文章
相關標籤/搜索