每次提到異步我都選擇繞開,感覺深不可測,最近打算看看異步,但又不願意看書,網上找了幾個視頻看,發現傳智播客的老師講異步都不是很深入,關鍵的問題一筆帶過,倒是把我弄糊塗了,印象最深刻的是那個老師說的一句話:“在異步函數中,Await之后會自動創建出一個線程”。確實,在控制台程序中是這樣,但是在winform中卻不都在UI線程中執行,感覺內部隱藏不可告人的秘密,索性自己探索一下。
控制台程序:
static void Main(string[] args) { Console.WriteLine("1.同步ThreaID:" + Thread.CurrentThread.ManagedThreadId); RunTimeAsync(); Console.WriteLine("4.異步執行完畢ThreaID:" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("5.同步延遲完畢ThreaID:" + Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } public async static Task RunTimeAsync() { Console.WriteLine("2.進入異步ThreaID:" + Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Console.WriteLine("3.異步執行后ThreaID:" + Thread.CurrentThread.ManagedThreadId); }
執行結果:
同樣的代碼在winform中執行:
winform程序:
private void button1_Click(object sender, EventArgs e) { Console.WriteLine("1.同步ThreaID:" + Thread.CurrentThread.ManagedThreadId); RunTimeAsync(); Console.WriteLine("4.異步執行完畢ThreaID:" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(2000); Console.WriteLine("5.同步延遲完畢ThreaID:" + Thread.CurrentThread.ManagedThreadId); } public async static Task RunTimeAsync() { Console.WriteLine("2.進入異步ThreaID:" + Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Console.WriteLine("3.異步執行后ThreaID:" + Thread.CurrentThread.ManagedThreadId); }
執行結果:
前后代碼執行結果對比后發現,
1.線程數量,winfrom都是在一個線程里執行,控制台程序是兩個線程執行,其中異步操作在新的線程里。
2.執行順序,winform先執行click中同步操作,再執行異步操作,控制台卻相反。
所以await這個關鍵字到底對異步操作動了什么手腳呢?
在查了一些相關資料后有兩個詞比較高頻:SynchronizationContext 和TaskScheduler,執行代碼時,Task不會自己執行,而是將執行內容放到SynchronizationContext 和TaskScheduler執行。
其實await Task.Delay(1000)或者await AsyncFun()(異步操作)等價於以下代碼:
Task t = AsyncFun();
var currentContext = SynchronizationContext.Current;
if (null == currentContext)
{
t.ContinueWith((te) => { Run(); }, TaskScheduler.Current);
}
else
{
t.ContinueWith((te) => { Run(); }, TaskScheduler.FromCurrentSynchronizationContext())
}
如果SynchronizationContext為null就在TaskScheduler中執行,如果不為null則在FromCurrentSynchronizationContext中執行。
先了解一下TaskScheduler--------CSDN中注解是:表示一個處理將任務排隊到線程中的低級工作的對象。
看一下TaskScheduler的元數據:
TaskScheduler其實就是將Task放到Threadpool執行,通過入隊出隊的方式排列執行順序。
根據等價代碼我們要區分一下控制台程序與winfrom程序異步操作的執行方式,
我們執行這條代碼:var currentContext = SynchronizationContext.Current;
在控制台程序中currentContext 為null
在winform中currentContext 為System.Windows.Forms.WindowsFormsSynchronizationContext
那么控制台程序是在TaskScheduler.Current所在的線程池執行,我們看到控制台的await 異步操作在其他線程中執行一致。
下面分析一下winform的執行方式:
WindowsFormsSynchronizationContext繼承自SynchronizationContext,
https://www.cnblogs.com/lzxianren/p/SynchronizationContext.html 參考了這篇文章
里面有兩點:
public virtual void Send(SendOrPostCallback d, Object state)
{
d(state);
}
public virtual void Post(SendOrPostCallback d, Object state)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}
在UI線程執行是,會將執行的任務用Send方法放到消息泵中,當時UI中同步執行的操作執行完畢,再執行詳細泵中的數據,這就是為什么UI中只在一個線程中執行,而且總是在最后執行。
總結:用 await 語句等待一個任務完成,當該方法在 await 處暫停時,就可以捕捉上下文(context)。如果當前 SynchronizationContext 不為空,這個上下文就是當前SynchronizationContext;如果當前 SynchronizationContext 為空,則這個上下文為當前askScheduler,該方法會在這個上下文中繼續運行。一般來說,運行 UI 線程時采用 UI 上下文,處理 ASP.NET 請求時采用 ASP.NET 請求上下文,其他很多情況下則采用線程池上下文。因此,在上面的代碼中,每個同步程序塊會試圖在原始的上下文中恢復運行。如果在 UI線程中調用 ,這個方法的每個同步程序塊都將在此 UI 線程上運行。但是,如果在線程池線程中調用,每個同步程序塊將在線程池線程上運行。
控制台程序以及類庫是在TaskScheduler中調度執行,UI程序則是在SynchronizationContext調度執行。
參考了:https://blog.csdn.net/wlk1229/article/details/81287374