標籤:

如何正確理解.NET 4.5和C# 5.0中的async/await非同步編程模式?

最近在看《C#圖解教程》,其中第20章講解了.NET 4.5和C# 5.0新添加的非同步編程模型async/await,舉了一個在GUI中執行非同步操作的例子:

private async void btnDoStuff_Click(object sender, RoutedEventArgs e)
{
btnDoStuff.IsEnabled = false;
lblStatus.Content = "Doing Stuff";

await Task.Delay(4000);

lblStatus.Content = "Not Doing Anything";
btnDoStuff.IsEnabled = true;
}

上面這個方法是一個按鈕空間Click事件的事件響應代碼。

我的問題是:按照我的理解,控制項的響應代碼應該是在GUI線程裡面被調用的,而且對於GUI應用程序來說,GUI線程一般只有一個,並且所有和GUI控制項方面的交互都應該通過GUI線程來完成。那麼用await修飾的非同步方法是在哪個線程中被調用的?為什麼上面這個事件處理方法不會阻塞GUI?

我還看到其它一些描述是說使用async/await非同步模式不會生成新的線程,那麼只在原來已有線程的基礎上面如何做到非同步運行?

總之,如何正確理解.NET 4.5和C# 5.0中的async/await非同步編程模式?


這個await,其實只是把對老版本C#迭代器的慣用法官方化了。現在很多平台都因為一些原因不得不用舊版本的C#,比如unity,想非同步那隻能通過迭代器來做。

async、迭代器都是語法糖,編譯器會幫你實現成一個狀態機匿名類,實例裡面hold住一些臨時變數,記錄一下當前狀態。根據你寫的yield/await,把一個非同步方法拆成幾個同步block,根據一定規則定期的去MoveNext一下,Current是Task那我就根據你配置的線程上下文決定把這個Task跑在哪個線程上。

那麼用await修飾的非同步方法是在哪個線程中被調用的?為什麼上面這個事件處理方法不會阻塞GUI?

我還看到其它一些描述是說使用async/await非同步模式不會生成新的線程,那麼只在原來已有線程的基礎上面如何做到非同步運行?

題主這個例子這個方法就是在UI線程調用的,並且沒有ConfigureAwait(false),所以會在當前await時捕捉的UI線程上下文執行之後的同步block。

至於為什麼不會阻塞,可以簡單理解為執行了第一個block,碰到Delay(4000),給UI線程的定時器掛一個4000時間之後再調用下一個同步塊的回調。

看題主說的書名像是國產的書,這方面還是看《CLR via C#》或者《Concurrency in C# cookbook》比較好。


await修飾的方法返回的是一個Task,而這個Task其實就是一個非同步句柄,如果我來取名字的話多半就叫做IAsyncHandler。

一個IAsyncHandler你可以想像成是這麼一個東西:

public interface IAsyncHandler
{
Register( Action continuation );
}

這是偽代碼,事實上不存在這麼個東西。

註冊一個回調方法在非同步操作完成後繼續,所以事實上這段代碼的原理像是這樣的:

private async void btnDoStuff_Click(object sender, RoutedEventArgs e)
{
btnDoStuff.IsEnabled = false;
lblStatus.Content = "Doing Stuff";

var handler = Task.Delay(4000) as IAsyncHandler
handler.Register( () =&>
{
lblStatus.Content = "Not Doing Anything";
btnDoStuff.IsEnabled = true;
} );
}

當然上面全是偽代碼,但是如果你能看懂這段代碼在幹什麼,那麼async基本就可以懂了,剩下的只是一些實現細節上的問題。

通常情況下,Task.Delay會立即返回一個Task對象,這個Task對象會在指定時間之後被標記為Completed,而被標記Completed就會立即開一個線程來進行延續的操作。

但是這裡有個問題就是你這個方法是寫在UI線程裡面的,控制項的事件會被UI線程觸發,而UI線程上有個SynchronizationContext對象,這個對象的存在就會使得系統在非同步回調的時候去捕獲源線程。在原來的線程(UI線程)去執行延續的任務。

而我們知道WinForm裡面有個方法叫做Control.Invoke,可以把一個方法封送到UI線程去執行,而上面的工作和這個方法底層的原理其實是一樣的,所以,其實這段代碼用傳統的思維來理解的話像是這樣:

private async void btnDoStuff_Click(object sender, RoutedEventArgs e)
{
btnDoStuff.IsEnabled = false;
lblStatus.Content = "Doing Stuff";

Action continuation = () =&>
{
lblStatus.Content = "Not Doing Anything";
btnDoStuff.IsEnabled = true;
};

Thread.Start( () =&>
{
Thread.Sleep( 4000 );
Control.Invoke( continuation );
} );
}


因為btnDoStuff_Click函數在await剛開始執行的那一刻就已經返回了。await後面的代碼是這些事情做完了之後,重新射進GUI線程裡面跑的。

QueueUserAPC function (Windows)


參看C# in Depth第三版相關章節,await可以看作是Task.ContinueWith()的語法糖


這是個古老的問題,不過我還是想發表一些觀點。

一些同學可能認為await會創建新線程,這顯然是不正確的,因為它只是個語法糖。但是出現await的地方,確實有可能會創建新線程。我覺得我們不妨觀察一下await一個返回Task的async方法,和直接await一個new出來的Task,或者Task自帶的靜態方法創建的Task,是有些許不同的。

我們在await一個返回Task的async方法時,其實這麼寫純粹是方便編譯器做翻譯工作,標記出狀態機裡面要創建幾個狀態罷了。生成的代碼依然是同步工作的,只不過是經狀態機兜幾個圈子罷了。

但是await一個new出來的Task,或者Task自帶的靜態方法創建的Task,這個地方的Task裡面是真非同步,跑到另一個線程去工作了,另一個線程可能是線程池中一開始就有的,也可能是特意新建的。對於帶有GUI的應用,肯定是不會用UI線程運行Task裡面的代碼的。

編程經驗告訴我們,async和await正是從await一個Task(或者其他非同步操作對象)開始往外「傳染」的。當然我們依然可以通過手動管理來去掉傳染。


你首先要知道async await這是一個基於Task的非同步 盡量不阻塞當前線程,內部是否佔用線程是不確定的(取決於內部實現使用的非同步方式),當然從約定來說用await去等待的方法內部一定有IO非同步,而有用到await方法的本生一定有async標識,也就是不佔用任何線程的無阻塞等待。意思就是不阻塞任何線程的等待執行結束,當然不需要開線程了。

核心思想就是用了async await 以後,代碼和同步的代碼非常相似,代碼結構,異常處理等都比以往非同步便捷的多,因為幾乎和同步一樣。

基本用法就是方法上加上async ,然後調用TAP版本通常以Async結尾的方法,然後用await去等待他。這個方法其實就是個基於Task非同步寫的,這個方法內部可能是對AMP非同步模型進行了一次封裝。

此語法糖的主要目的還是為了讓你不要寫同步代碼,讓他很簡單的就改成IO非同步的代碼。 他在什麼時候開始非同步呢? (以AMP非同步模型來說)其實是在Async結尾的方法內部調用Begin非同步時才開始非同步,在此之前還是同步執行的。所以他能讓GUI程序大幅度減少阻塞,但不是最大可能減少阻塞。

初級用法就是按同步的代碼寫,順便加上async await 改成調用Async結尾的方法,減少線程阻塞,最適合web這種多請求並發的情況。

進階點就是可以多個非同步並發,等待他們同時完成。

再高階點就是配合高級的同步寫法,無CPU阻塞的完成非同步中的同步操作,這個就最好自己多研究了,大部分情況用不到,需要大量的非同步以及同步知識。


1. async、await拋去語法糖,內部是Task+狀態機。

2. 有時候會新開線程,有時候不會新開線程,可以用Thread.CurrentThread.ManagedThreadId測試下得知。

3. 你可以簡單把Async的執行步驟理解為: 主線程A邏輯+ 非同步任務線程B邏輯+主線程C邏輯。

按照2來說,這3個步驟是可能會使用同一個線程的,也可能會使用2個,甚至3個線程。

static void Main(string[] args)
{
Test();
Console.WriteLine("A邏輯");
Console.ReadLine();
}
static async void Test()
{
await Task.Run(() =&> { Thread.Sleep(1000); Console.WriteLine("B邏輯"); });
Console.WriteLine("C邏輯");
}

Async是可以用線性思維去理解的,比通知回調簡單些。


不請自來,一是希望可以提供一些有助於理解的信息,而是希望能從知乎大神那裡驗證我的理解。

假設環境是http://asp.net mvc

非同步action執行到await之前都是再loop線程,loop線程池的線程數是有限的,在iis線程池的高級設置(queue length)里可以設置,一旦執行到await之後,就會開啟另一個線程去執行await的任務,同時,loop線程回池去接收其他請求,當await執行完了又會從loop線程池拿一個線程處理接下來的任務。這樣做的好處是不回阻塞loop線程,提高並發(並不是提高單個action的執行效率)。await的任務也最好是高io,低計算的任務(比如文件讀寫,網路請求,資料庫讀寫)。這樣比較划算


不會創建新線程,整體結構類似於一個狀態機,編譯器將生成一個私有的內嵌結構,用一個流程式控制制的方法作為入口,任何await表達式都可以認為是執行路徑的一個分支。

首先,檢查非同步操作的IsCompleted屬性,如果返回true,立即繼續。如果不是,會存儲awaiter,更新狀態,為awaiter附加後續操作等,以供後續操作。

具體實現細節可參閱 深入理解C#(C# indepth)第三版15章


使用 Async 和 Await 的非同步編程(C# 和 Visual Basic)

看看這個就知道了,


鑒於題主對代碼格式進行了修改,我搜搜了一下,在一個文章(Async和Await非同步編程的原理)中看到以下內容:

回過頭來看我們講的主題:非同步編程。在學習使用async/await的時候,很多文檔包括msdn都刻意提到async/await關鍵字不會創建新的線程,用async關鍵字寫的函數中的代碼都在調用線程中執行。這裡是最容易混淆的地方,嚴格意義上這個說法不準確,非同步編程必然是多線程的。msdn文檔里提到的不會創建新線程是指async函數本身不會直接在新線程中運行。通過上面的分析,我們知道本質上是await調用的非同步函數執行完成後回調狀態機的MoveNext來執行餘下未執行完成的代碼,await調用的非同步函數必然在某個地方——也許是嵌套了很深的一個地方——啟動了一個新的工作線程來完成導致我們要使用非同步調用的耗時比較長的工作,比如網路內容讀取。

看大意是,實際上系統內部有個類似於線程池的東東,這個應該是在系統內部創建的,在這個線程池中執行非同步操作.

你那個書中的誤導成分太大,MSDN也是. 非同步的東西肯定在非UI線程中執行,否則那兩個關鍵字就沒有意義了,至於你看的書中說"不會新開線程",說得不明白,應該是使用系統自帶的類似線程池的東西,所以說不會新開線程.

另外,我做Java十多年,.net只做了一年多,不過這些東西應該都是相通的. Java和C#比,麻煩很多.

------------------------------------------------------------------------------------------------------------

任何編程問題,如果代碼沒有縮進,我都懶得看了. 這個不答了.


為什麼不阻塞?因為await的時候方法返回了。 如何做到非同步執行?await的時候的確開啟了一個新的線程,所以是非同步。

lblStatus.Content為什麼可以正常執行?因為await這句默認捕獲當前上下文環境,即採用GUI線程來執行await之後的代碼。

async/await不會自動開啟新線程?如果await你自己定義的方法,當然不會自動開啟新線程。


推薦閱讀:

在現在的條件下使用Node.js開發複雜業務邏輯的ERP應用可行嗎?
ASP.NET MVC 最好的視圖引擎是什麼?
微軟宣布 .NET 開源了,如何學好.NET?
微軟開源 .NET 和其他相關項目會造成什麼影響?
學了C#語言可以從事哪些工作?

TAG:NET | 非同步 | C# |