前言
之前寫過有關異步的文章,對這方面一直比較弱,感覺還是不太理解,於是會花點時間去好好學習這一塊,我們由淺入深,文中若有敘述不穩妥之處,還請批評指正。
話題
(1)是不是將方法用async關鍵字標識就是異步方法了呢?
(2)是不是沒有await關鍵字的存在async就沒有存在的意義了呢?
(3)用異步方法的條件是什么呢,為什么會有這個條件限制?
(4)只能調用.NET Framework內置的用await標識的Task,能否自定義實現呢?
(5)在lambda表達式中是否可以用async和await關鍵字來實現異步呢(即異步lambda表達式)?
上述拋出這幾個話題,明白本文主要講述的話題以及需要深入了解的知識。
注意:這里我將參照園友【反骨仔】的文章進行進一步解析。
async關鍵字
例如異步方法是這樣的:
public static async Task<int> asyncMethod() { return await Task.Run(() => Calculate()); } static int Calculate() { return 1 + 1; }
那要是如下這樣寫呢?
public static async Task<int> asyncMethod() { var task = Task.Run(() => Calculate()); return task.Result; }
那上述這種寫法是不是也是異步方法呢?答案是【NO】,既然不是異步方法為什么要用async關鍵字來進行標識呢?不是很容易被我們所誤解呢?好了疑問這么多我們一一來解惑。
當方法用async標識時,編譯器主要做了什么呢?
(1)告訴編譯器這個方法里面可能會用到await關鍵字來標識該方法是異步的,如此之后,編譯器將會在狀態機中編譯此方法。接着該方法執行到await關鍵字時會處於掛起的狀態直到該異步動作完成后才恢復繼續執行方法后面的動作。
(2)告訴編譯器解析出方法的結果到返回類型中,比如說Task或者Task<TResult>,也就是說將返回值存儲到Task中,如果返回值為void那么此時應該會將可能出現的異常存儲到上下文中。
當方法用async標識時,是不是所有調用者都將是異步的呢?
當將方法用async標識時且返回值為void或者Task或者Task<TReuslt>,此時該方法會在當前線程中一直同步執行。用async標識方法並不會影響方法運行完成是否是同步或者異步,相反,它能夠將方法划分成多塊,有可能有些在異步中運行,以至於這些方法是異步完成的,而划分異步和同步方法的邊界就是使用await關鍵字。也就是說如果在方法中未用到await關鍵字時則該方法就是一整塊沒有所謂的划分,會在同步中運行,在同步中完成。
當方法用async標識時,是否會引起方法的調用會被添加到線程池隊列中或者是創建一個新的線程呢?
顯然不是這樣,當用async標識方法時只是顯示告訴編譯器在該方法中await關鍵字可能會被用到,當執行到await關鍵字開始處於掛起的狀態知道異步動作執行完成才恢復(異步操作是在狀態機中【有關狀態機請看這里:Async和Await異步編程的原理】完成,完成后此時才會創建一個線程),這也就是為什么在方法中方法用async標識如果沒有用到await關鍵字IDE會發出警告的原因。
—————————————————————————————————————————————————————————————————
到了這里我們可以得出結論:無論方法是同步還是異步都可以用async關鍵字來進行標識,因為用async標識只是顯示表明在該方法內可能會用到await關鍵字使其變為異步方法,而且將該異步方法進行了明確的划分,只有用了await關鍵字時才是異步操作,其余一並為同步操作。
參數為什么不能使用ref和out關鍵字
返回類型必須為void或者Task或者Task<TResult>和關鍵字的標識以及其他就不再敘述,其中有一條是不能使用ref和out關鍵字,你是背書似的銘記了這一條,還是略加思索了呢?你想過沒有為何不可呢?
我們知道用ref和out關鍵字不過是為了在方法里面改變其值,也就是是當同步完成時我們期望被ref或者out關鍵字修飾的值會被設置,但是它們可能在異步完成時或者之后才會被設置達不到我們預期,所以在異步方法中不能用ref和out關鍵字。
lambda表達式是否可以異步呢?
返回類型Task參數可以為lambda表達式或者匿名方法對象,那直接對lambda表達式異步是否可行?下面我們來看看
public static async Task<T2> CallFuncAsync<T1, T2>(T1 t, Func<T1, T2> func) { return func.Invoke(t); } public static async Task<string> GetStringAsync(int value) { return await Task.Run(() => "xpy0928"); } public static async Task MainAsync() { string value = await CallFuncAsync<int, string>(30, async (s) => await GetStringAsync(s)); }
編譯后生成如下錯誤:
由上知異步lambda表達式是不行的,猜測是異步lambda表達式不能轉換為表達式樹,同時我們看看上述代碼,CallFunAsync此時並未是異步方法,上述我們已經敘述過,此時是同步運行,既然上述錯誤,並且代碼也有不可取之處我們接下來一一進行修改。
string value = await CallFuncAsync<int, string>(30, async (s) => await GetStringAsync(s)); 修改為: string value = await CallFuncAsync<int, string>(30, s => GetStringAsync(s).Result);
解決了編譯錯誤,但是未解決CallFuncAsync為異步運行,我們將其修改為異步運行。既然await是針對於Task而操作,我們將CallFuncAsync中的返回參數設置為Task即可。
public static async Task<T2> CallFuncAsync<T1, T2>(T1 t, Func<T1, Task<T2>> func) { return await func.Invoke(t); }
則最終調用時我們直接調用即可。
string value = await CallFuncAsync(30, GetStringAsync);
此時CallFuncAsync才算是異步運行。
補充(2016-10-21 23:11)
對於異步表達式有一點其實表述不太正確,其實我一直還是有點懷疑異步lambda表達式真的不行嗎,此刻我居然發現這樣是可以的:
var task = Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(5000)); });
如上不正是異步表達式的影子嗎,於是我將上述代碼進行了改寫,如下:
public static async Task<Action> CallFuncAsync(Action action) { return action; } public static async Task<Action> GetStringAsync() { return () => Console.WriteLine("xpy0928"); } public static async Task MainAsync() { await CallFuncAsync(async () => await GetStringAsync()); }
此時編譯通過,說明表述異步表達式並非一定不可以,只是對於無參數的lambda表達式才可以,而對於有參數的lambda表達式如fun則不能執行異步lambda表達式。
至此可以基本下結論:
異步lambda表達式只對於無參數的lambda表達式 才可以(當然這也就沒有了什么意義),而對於有參數的lambda表達式則產生編譯錯誤則不能執行異步(猜測是無法轉換成對應的表達式樹)。(不知是否嚴謹或者不妥,若有錯誤之處,還望對此理解的更透徹的園友給出批評性意見)。
為了驗證這一點,我們來看看無參數的func委托例子,如下:
static async Task<string> GetTaskAsync() { await Task.Delay(TimeSpan.FromMilliseconds(5000)); return "xpy0928"; }
var task = Task.Run(async () => await GetTaskAsync());
此時無參數的func委托則編譯通過,應該是驗證了上述觀點(還是有點懷疑我所下的結論)。
await關鍵字
await關鍵字是這樣用的
await Task.Run(() => "xpy0928");
此時背后究竟發生了什么呢?我們上述也說過異步動作時在狀態機中完成,當執行到這里時,編譯器會自動生成代碼來檢測該動作是否已經完成,如果已經完成則繼續同步執行await關鍵字后面的代碼,通過判斷其狀態機狀態若未完成則會掛起一個繼續的委托為await關鍵字的對象直到完成為止,調用這個繼續動作的委托重新進入未完成的這樣一個方法。
比如說: await someObject; 編譯器則會生成如下代碼:
private class FooAsyncStateMachine : IAsyncStateMachine { // Member fields for preserving “locals” and other necessary state int $state; TaskAwaiter $awaiter; … public void MoveNext() { // Jump table to get back to the right statement upon resumption switch (this.$state) { … case 2: goto Label2; … } … // Expansion of “await someObject;” this.$awaiter = someObject.GetAwaiter(); if (!this.$awaiter.IsCompleted) { this.$state = 2; this.$awaiter.OnCompleted(MoveNext); return; Label2: } this.$awaiter.GetResult(); … } }
此時講到這里就要涉及到await背后具體的實現,在Task或者Task<TResult>類里面有這樣一個返回類型為 TaskAwaiter 的 GetAwaiter 屬性,而TaskAwaiter中有如下屬性:
public struct TaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion { public bool IsCompleted { get; } public void GetResult(); public void OnCompleted(Action continuation); public void UnsafeOnCompleted(Action continuation); }
通過IsComplete來判斷是否已經完成。這個有什么作用呢?通過看到背后具體實現,我們可以自己簡單實現異步擴展方法,當我們在Task中查看其方法會有這樣的提示:
下面我們就來實現這樣的效果,給TimeSpan添加異步方法:
public static class Extend { public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan) { return Task.Delay(timeSpan).GetAwaiter(); } }
此時異步方法則是這樣的:
總結
本節我們詳細講述了async和await關鍵字的使用和一些基本原理以及解釋其原因,希望通過對本文的學習,對大家能夠更好的去理解異步,我也在學習中,Over。