在async和await以前咱們用Task來實現異步任務是這樣作的:編程
static Task<string> GetBaiduHtmlTAP() {
//建立一個異步Task對象,內部封裝了異步任務邏輯 return new Task<string>(() => { Console.WriteLine("GetBaiduHtml 1:" + Thread.CurrentThread.ManagedThreadId); var httpRequest = WebRequest.CreateHttp("http://www.baidu.com"); var response = httpRequest.GetResponse(); Console.WriteLine("GetBaiduHtml 2:" + Thread.CurrentThread.ManagedThreadId); //var response = httpRequest.GetResponse(); var streamReader = new StreamReader(response.GetResponseStream()); //var bodyText = await streamReader.ReadToEndAsync(); var bodyText = streamReader.ReadToEnd(); //Console.WriteLine(bodyText); return bodyText; }); }
這樣來調用:
var task = GetBaiduHtmlTAP(); task.Start();//在線程池中啓動異步任務 task.Wait();//等待異步任務結束,此時當前線程阻塞在這裏
Console.WriteLine(task.Result);//獲取異步任務結果。
而使用async和await後的異步任務是這樣寫的:異步
static async Task<string> GetBaiduHtmlAsync() { Console.WriteLine("GetBaiduHtml 1:" + Thread.CurrentThread.ManagedThreadId); var httpRequest = WebRequest.CreateHttp("http://www.baidu.com"); var response = await httpRequest.GetResponseAsync();
//經過查看線程id,await以後的代碼是在一個新的線程中運行的。也就是說當調用await的時候,當前線程會退出執行,await的task完成後在新的線程運行後續代碼
Console.WriteLine("GetBaiduHtml 2:" + Thread.CurrentThread.ManagedThreadId);
var streamReader = new StreamReader(response.GetResponseStream());
var bodyText = streamReader.ReadToEnd(); return bodyText; }
調用的時候這樣作:
string result = await GetBaiduHtmlAsync();
Console.WriteLine(result);async
其實不單單是寫法變了,而是執行原理,控制權模型也發生了變化。引用官方的圖:ide
上圖第6步執行完await後,線程的控制權就返回給調用者了,而不是阻塞。詳見函數
在使用中有以下問題一直很疑惑。工具
1. 爲何async方法的返回值是Task對象,可是咱們在方法體內部返回的卻能夠是Task的Result對象呢。(惟一能解釋該問題的只能是編譯器針對async方法特殊處理了。)測試
2. 爲何若是要在方法中使用await,方法必需要標示爲asyncui
3. 通常咱們本身建立的Task對象須要手動去調用run方法才能啓動任務。那async方法返回的Task對象爲何不須要手動調用run方法task所封裝的代碼就運行了呢。(一開始是覺得對task執行await關鍵字的時候會調用run方法,可是其實就算不執行await,async方法返回的task對象就已是在運行狀態了。)this
4. 當咱們await異步任務後,爲何異步任務結束以後能夠在新的線程中繼續運行await以後的代碼spa
5. 經過測試能夠知道,GetBaiduHtmlAsync方法中await調用前面的代碼是在函數調用者的線程中運行,而await調用後面的代碼是在另外一個線程中運行。
若是編譯器是將方法體內部整個在一個Task中運行,那await調用前面的代碼不該該在函數調用者線程中運行。
要解釋這些問題,最直接的辦法就是查看編譯器生成的代碼。
問題1
把上面的async函數代碼編譯後,再經過反編譯工具反編譯獲得以下結果:
經過圖片咱們能夠看到:
1. 生成了一個新的類:<GetBaiduHtmlAsync>d_2。
2. 而GetBaiduHtmlAsync方法則被替換爲編譯器產生的代碼。
先看看這個編譯器重寫的方法的簽名:沒有了async關鍵字,返回值爲Task<string>,而在方法體裏面返回的是:d__.<>t_builder.Task,而且在返回值前調用了<>t_builder的Start方法。
經過這段代碼能夠回答上面第1,3個問題了。
async方法裏不用返回Task對象確實是編譯器的語法糖,編譯器會爲咱們生成返回Task對象的代碼。至於Task的狀態,咱們能夠繼續看上面生成的代碼:
新生成的類<GetBaiduHtmlAsync>d_2實現了IAsyncStateMachine接口,能夠認爲是一個狀態機。而其中的屬性<>t__builder的類型是AsyncTaskMethodBuilder<T>,MSDN上對它的描述是
Represents a builder for asynchronous methods that returns a task and provides a parameter for the result
也就是說提供將異步方法轉換爲Task的功能,再看看它的Start方法的定義:
public void Start<TStateMachine>( ref TStateMachine stateMachine ) where TStateMachine : IAsyncStateMachine
Begins running the builder with the associated state machine.
理解爲基於傳入的狀態機啓動有Builder產生的Task。
所以上面編譯器生成的GetBaiduHtmlAsync方法的所作的事情是:
1. 實例化一個狀態機<GetBaiduHtmlAsync>d_2,並將狀態設置爲-1(咱們本身所定義的GetBaiduHtmlAsync方法體應該被移入這個狀態機種,後面描述)
2. 建立了一個AsyncTaskMethodBuilder<string>實例來產生異步任務的Task對象
3. 基於Step1中建立的狀態機啓動Step2中建立的builder所生成的Task。
4. 將Task對象返回給調用者
至此能夠解釋上面第3個問題。Task在返回給調用者時已經被啓動。可是發現一個問題,GetBaiduHtmlAsync方法返回的task對象的status屬性並非Running狀態,而是WaitingForActivation。貌似第3個問題並非我想象的那樣。接着往下看。
問題4
那咱們本身定義的GetBaiduHtmlAsync中的代碼去哪了呢?很明顯應該被轉換爲狀態機,被移植到<GetBaiduHtmlAsync>d_2類中了。下面是它的代碼:
private struct <GetBaiduHtmlAsync>d__2 : IAsyncStateMachine { // Fields public int <>1__state; public AsyncTaskMethodBuilder<string> <>t__builder; private TaskAwaiter<WebResponse> <>u__1; // Methods private void MoveNext() { string str; int num = this.<>1__state; try { TaskAwaiter<WebResponse> awaiter; if (num != 0) { Console.WriteLine("GetBaiduHtml 1:" + Thread.CurrentThread.ManagedThreadId); awaiter = WebRequest.CreateHttp("http://www.baidu.com").GetResponseAsync().GetAwaiter(); if (!awaiter.IsCompleted) { this.<>1__state = num = 0; this.<>u__1 = awaiter; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<WebResponse>, Program.<GetBaiduHtmlAsync>d__2>(ref awaiter, ref this); return; } } else { awaiter = this.<>u__1; this.<>u__1 = new TaskAwaiter<WebResponse>(); this.<>1__state = num = -1; } WebResponse result = awaiter.GetResult(); awaiter = new TaskAwaiter<WebResponse>(); Console.WriteLine("GetBaiduHtml 2:" + Thread.CurrentThread.ManagedThreadId); str = new StreamReader(result.GetResponseStream()).ReadToEnd(); } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(str); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { this.<>t__builder.SetStateMachine(stateMachine); } }
這裏實現的大體邏輯爲:
1. 狀態機初始狀態爲-1,所以上面的builder啓動任務時會建立獲取Http的Response的異步Task
2. 設置狀態機狀態爲0,表示在等待http task的結果。這裏有個關鍵調用
3. 當http task任務結束,獲取http task的result,從中讀取http response的body,同時將狀態機狀態設爲-1
4. 將http response的body設置builder關聯的task的result,設置狀態機狀態爲-2
上面的過程當中有一個關鍵調用:
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<WebResponse>, Program.<GetBaiduHtmlAsync>d__2>(ref awaiter, ref this);
這個調用會去等待awaiter,awaiter完成後會調用狀態機的moveNext方法將狀態機移到下一個狀態。這個調用推進了上面狀態機的轉換。
從上面的邏輯能夠看出,編譯器將咱們的代碼以await調用爲分隔生成了一個狀態機,await調用前的代碼置於狀態-1中,await調用後的代碼置於狀態0中。經過這種方式來實現異步任務完成後繼續運行await後面
的代碼。至此解釋了上面的第4個問題。其實和回調效果同樣,只不過使用await是代碼可讀性更好。
問題3,5
接着咱們再看前面提到過的AsyncTaskMethodBuilder<T>的Start方法:
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine { if (((TStateMachine) stateMachine) == null) { throw new ArgumentNullException("stateMachine"); } ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher(); RuntimeHelpers.PrepareConstrainedRegions(); try { ExecutionContext.EstablishCopyOnWriteScope(ref ecsw); stateMachine.MoveNext(); } finally { ecsw.Undo(); } }
這個方法中調用了狀態機的MoveNext方法,至關於啓動了狀態機。而狀態機的第一個狀態會運行await前面的代碼。因此,GetBaiduHtmlAsync方法中await前面的代碼的運行並非運行其返回的task的結果。而是這裏AsyncTaskMethodBuilder的Start方法調用了MoveNext方法的結果。而且是同步調用,所以是運行在GetBaiduHtmlAsync方法的調用線程裏。由此能夠看出,在Async&Await編程模型中,Task其實只是表達異步任務和承載任務狀態和結果,而任務的運行其實經過狀態機的狀態改變來推進。至此解釋了第3和第5個問題。
問題2
至於第2個問題,根據上面反編譯的代碼,await關鍵字被編譯器翻譯爲
1. 獲取異步任務的awaiter
2. 基於awaiter調用
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<WebResponse>, Program.<GetBaiduHtmlAsync>d__2>(ref awaiter, ref this);而這個調用是依賴根據async所生成的一整套代碼(狀態機,task builder...)因此await必須在async的環境中使用。