1. 程式人生 > >理解C#中的 async await

理解C#中的 async await

#### 前言 一個老掉牙的話題,園子裡的相關優秀文章已經有很多了,我寫這篇文章完全是想以自己的思維方式來談一談自己的理解。(PS:文中涉及到了大量反編譯原始碼,需要靜下心來細細品味) ## 從簡單開始 為了更容易理解這個問題,我們舉一個簡單的例子:用非同步的方式在控制檯上分兩步輸出“Hello World!”,我這邊使用的是Framework 4.5.2 ``` class Program { static async Task Main(string[] args) { Console.WriteLine("Let's Go!"); await TestAsync(); Console.Write(" World!"); } static Task TestAsync() { return Task.Run(() => { Console.Write("Hello"); }); } } ``` ## 探究反編譯後的原始碼 接下來我們使用 .NET reflector (也可使用 dnSpy 等) 反編譯一下程式集,然後一步一步來探究 async await 內部的奧祕。 ### Main方法 ``` [DebuggerStepThrough] private static void
(string[] args) { Main(args).GetAwaiter().GetResult(); } [AsyncStateMachine(typeof(
d__0)), DebuggerStepThrough] private static Task Main(string[] args) {
d__0 stateMachine = new
d__0 { <>t__builder = AsyncTaskMethodBuilder.Create(), args = args, <>1__state = -1 }; stateMachine.<>t__builder.Start<
d__0>(ref stateMachine); return stateMachine.<>t__builder.Task; } // 實現了 IAsyncStateMachine 介面 [CompilerGenerated] private sealed class
d__0 : IAsyncStateMachine { // Fields public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public string[] args; private TaskAwaiter <>u__1; // Methods private void MoveNext() { } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } } ``` 臥槽!竟然有兩個 Main 方法:一個同步、一個非同步。原來,雖然我們寫程式碼時為了在 Main 方法中方便非同步等待,將 void Main 改寫成了async Task Main,但是實際上程式入口仍是我們熟悉的那個 void Main。 另外,我們可以看到非同步 Main 方法被標註了`AsyncStateMachine`特性,這是因為在我們的原始碼中,該方法帶有修飾符`async`,表示該方法是一個非同步方法。 好,我們先看一下非同步Main方法內部實現,它主要做了三件事: 1. 首先,建立了一個型別為`
d__0`的狀態機 stateMachine,並初始化了公共變數 **<>t__builder**、args、**<>1__state = -1** - <>t__builder:負責非同步相關的操作,是實現非同步 Main 方法非同步的核心 - <>1__state:狀態機的當前狀態 2. 然後,呼叫`Start`方法,藉助 stateMachine, 來執行我們在非同步 Main 方法中寫的程式碼 3. 最後,將指示非同步 Main 方法執行狀態的`Task`物件返回出去 ### Start 首先,我們先來看一下`Start`的內部實現 ``` // 所屬結構體:AsyncTaskMethodBuilder [SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable] public void Start(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(); } } ``` 我猜,你只能看懂`stateMachine.MoveNext()`,對不對?好,那我們就來看看這個狀態機類`
d__0`,並且著重看它的方法`MoveNext`。 ### MoveNext ``` [CompilerGenerated] private sealed class
d__0 : IAsyncStateMachine { // Fields public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public string[] args; private TaskAwaiter <>u__1; // Methods private void MoveNext() { // 在 Main 方法中,我們初始化 <>1__state = -1,所以此時 num = -1 int num = this.<>1__state; try { TaskAwaiter awaiter; if (num != 0) { Console.WriteLine("Let's Go!"); // 呼叫 TestAsync(),獲取 awaiter,用於後續監控 TestAsync() 執行狀態 awaiter = Program.TestAsync().GetAwaiter(); // 一般來說,非同步任務不會很快就完成,所以大多數情況下都會進入該分支 if (!awaiter.IsCompleted) { // 狀態機狀態從 -1 流轉為 0 this.<>1__state = num = 0; this.<>u__1 = awaiter; Program.
d__0 stateMachine = this; // 配置 TestAsync() 完成後的延續 this.<>t__builder.AwaitUnsafeOnCompletedd__0>(ref awaiter, ref stateMachine); return; } } else { awaiter = this.<>u__1; this.<>u__1 = new TaskAwaiter(); this.<>1__state = num = -1; } awaiter.GetResult(); Console.Write(" World!"); } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } } ``` ![](https://img2020.cnblogs.com/blog/1010000/202101/1010000-20210120165435649-2140245165.png) 先簡單理一下內部邏輯: 1. 設定變數 num = -1,此時 num != 0,則會進入第一個if語句, 2. 首先,執行`Console.WriteLine("Let's Go!")` 3. 然後,呼叫非同步方法`TestAsync`,`TestAsync`方法會在另一個執行緒池執行緒中執行,並獲取指示該方法執行狀態的 awaiter 4. 如果此時`TestAsync`方法已執行完畢,則像沒有非同步一般: 1. 繼續執行接下來的`Console.Write(" World!")` 2. 最後設定 <>1__state = -2,並設定非同步 Main 方法的返回結果 5. 如果此時`TestAsync`方法未執行完畢,則: 1. 設定 <>1__state = num = 0 2. 呼叫`AwaitUnsafeOnCompleted`方法,用於配置當`TestAsync`方法完成時的延續,即`Console.Write(" World!")` 3. 返回指示非同步 Main 方法執行狀態的 Task 物件,由於同步 Main 方法中通過使用`GetResult()`同步阻塞主執行緒等待任務結束,所以不會釋放主執行緒(廢話,如果釋放了程式就退出了)。不過對於其他子執行緒,一般會釋放該執行緒 大部分邏輯我們都可以很容易的理解,唯一需要深入研究的就是`AwaitUnsafeOnCompleted`,那我們接下來就看看它的內部實現 ### AwaitUnsafeOnCompleted ``` // 所屬結構體:AsyncTaskMethodBuilder [__DynamicallyInvokable] public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: ICriticalNotifyCompletion where TStateMachine: IAsyncStateMachine { this.m_builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); } // 所屬結構體:AsyncTaskMethodBuilder [SecuritySafeCritical, __DynamicallyInvokable] public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: ICriticalNotifyCompletion where TStateMachine: IAsyncStateMachine { try { // 用於流轉狀態機狀態的 runner AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null; Action completionAction = this.m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize); if (this.m_coreState.m_stateMachine == null) { // 此處構建指示非同步 Main 方法執行狀態的 Task 物件 Task builtTask = this.Task; this.m_coreState.PostBoxInitialization((TStateMachine) stateMachine, runnerToInitialize, builtTask); } awaiter.UnsafeOnCompleted(completionAction); } catch (Exception exception) { AsyncMethodBuilderCore.ThrowAsync(exception, null); } } ``` 咱們一步一步來,先看一下`GetCompletionAction`的實現: ``` // 所屬結構體:AsyncMethodBuilderCore [SecuritySafeCritical] internal Action GetCompletionAction(Task taskForTracing, ref MoveNextRunner runnerToInitialize) { Action defaultContextAction; MoveNextRunner runner; Debugger.NotifyOfCrossThreadDependency(); // ExecutionContext context = ExecutionContext.FastCapture(); if ((context != null) && context.IsPreAllocatedDefault) { defaultContextAction = this.m_defaultContextAction; if (defaultContextAction != null) { return defaultContextAction; } // 構建 runner runner = new MoveNextRunner(context, this.m_stateMachine); // 返回值 defaultContextAction = new Action(runner.Run); if (taskForTracing != null) { this.m_defaultContextAction = defaultContextAction = this.OutputAsyncCausalityEvents(taskForTracing, defaultContextAction); } else { this.m_defaultContextAction = defaultContextAction; } } else { runner = new MoveNextRunner(context, this.m_stateMachine); defaultContextAction = new Action(runner.Run); if (taskForTracing != null) { defaultContextAction = this.OutputAsyncCausalityEvents(taskForTracing, defaultContextAction); } } if (this.m_stateMachine == null) { runnerToInitialize = runner; } return defaultContextAction; } ``` 發現一個熟悉的傢伙——**ExecutionContext**,它是用來給咱們延續方法(即`Console.Write(" World!");`)提供執行環境的,注意這裡用的是`FastCapture()`,該內部方法並未捕獲`SynchronizationContext`,因為不需要流動它。什麼?你說你不認識它?大眼瞪小眼?那你應該好好看看[《理解C#中的ExecutionContext vs SynchronizationContext》](https://www.cnblogs.com/xiaoxiaotank/p/13666913.html)了 接著來到`new MoveNextRunner(context, this.m_stateMachine)`,這裡初始化了 runner,我們看看建構函式中做了什麼: ``` [SecurityCritical] internal MoveNextRunner(ExecutionContext context, IAsyncStateMachine stateMachine) { // 將 ExecutionContext 儲存了下來 this.m_context = context; // 將 stateMachine 儲存了下來(不過此時為 null) this.m_stateMachine = stateMachine; } ``` 往下來到`defaultContextAction = new Action(runner.Run)`,你可以發現,最終咱們返回的就是這個 defaultContextAction ,所以這個`runner.Run`至關重要,不過彆著急,我們等用到它的時候我們再來看其內部實現。 最後,回到`AwaitUnsafeOnCompleted`方法,繼續往下走。構建指示非同步 Main 方法執行狀態的 Task 物件,設定當前的狀態機後,來到`awaiter.UnsafeOnCompleted(completionAction);`,要記住,入參 completionAction 就是剛才返回的`runner.Run`: ``` // 所屬結構體:TaskAwaiter [SecurityCritical, __DynamicallyInvokable] public void UnsafeOnCompleted(Action continuation) { OnCompletedInternal(this.m_task, continuation, true, false); } [MethodImpl(MethodImplOptions.NoInlining), SecurityCritical] internal static void OnCompletedInternal(Task task, Action continuation, bool continueOnCapturedContext, bool flowExecutionContext) { if (continuation == null) { throw new ArgumentNullException("continuation"); } StackCrawlMark lookForMyCaller = StackCrawlMark.LookForMyCaller; if (TplEtwProvider.Log.IsEnabled() || Task.s_asyncDebuggingEnabled) { continuation = OutputWaitEtwEvents(task, continuation); } // 配置延續方法 task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext, ref lookForMyCaller); } ``` 直接來到程式碼最後一行,看到延續方法的配置 ``` // 所屬類:Task [SecurityCritical] internal void SetContinuationForAwait(Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext, ref StackCrawlMark stackMark) { TaskContinuation tc = null; if (continueOnCapturedContext) { // 這裡我們用的是不進行流動的 SynchronizationContext SynchronizationContext currentNoFlow = SynchronizationContext.CurrentNoFlow; // 像 Winform、WPF 這種框架,實現了自定義的 SynchronizationContext, // 所以在 Winform、WPF 的 UI執行緒中進行非同步等待時,一般 currentNoFlow 不會為 null if ((currentNoFlow != null) && (currentNoFlow.GetType() != typeof(SynchronizationContext))) { // 如果有 currentNoFlow,那麼我就用它來執行延續方法 tc = new SynchronizationContextAwaitTaskContinuation(currentNoFlow, continuationAction, flowExecutionContext, ref stackMark); } else { TaskScheduler internalCurrent = TaskScheduler.InternalCurrent; if ((internalCurrent != null) && (internalCurrent != TaskScheduler.Default)) { tc = new TaskSchedulerAwaitTaskContinuation(internalCurrent, continuationAction, flowExecutionContext, ref stackMark); } } } if ((tc == null) & flowExecutionContext) { tc = new AwaitTaskContinuation(continuationAction, true, ref stackMark); } if (tc != null) { if (!this.AddTaskContinuation(tc, false)) { tc.Run(this, false); } } // 這裡會將 continuationAction 設定為 awaiter 中 task 物件的延續方法,所以當 TestAsync() 完成時,就會執行 runner.Run else if (!this.AddTaskContinuation(continuationAction, false)) { AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this); } } ``` 對於我們的示例來說,既沒有自定義 SynchronizationContext,也沒有自定義 TaskScheduler,所以會直接來到最後一個`else if (...)`,重點在於`this.AddTaskContinuation(continuationAction, false)`,這個方法會將我們的延續方法新增到 Task 中,以便於當 TestAsync 方法執行完畢時,執行 runner.Run ### runner.Run 好,是時候讓我們看看 runner.Run 的內部實現了: ``` [SecuritySafeCritical] internal void Run() { if (this.m_context != null) { try { // 我們並未給 s_invokeMoveNext 賦值,所以 callback == null ContextCallback callback = s_invokeMoveNext; if (callback == null) { // 將回調設定為下方的 InvokeMoveNext 方法 s_invokeMoveNext = callback = new ContextCallback(AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext); } ExecutionContext.Run(this.m_context, callback, this.m_stateMachine, true); return; } finally { this.m_context.Dispose(); } } this.m_stateMachine.MoveNext(); } [SecurityCritical] private static void InvokeMoveNext(object stateMachine) { ((IAsyncStateMachine) stateMachine).MoveNext(); } ``` 來到`ExecutionContext.Run(this.m_context, callback, this.m_stateMachine, true);`,這裡的 callback 是`InvokeMoveNext`方法。所以,當`TestAsync`執行完畢後,就會執行延續方法 runner.Run,也就會執行`stateMachine.MoveNext()`促使狀態機繼續進行狀態流轉,這樣邏輯就打通了: ``` private void MoveNext() { // num = 0 int num = this.<>1__state; try { TaskAwaiter awaiter; if (num != 0) { Console.WriteLine("Let's Go!"); awaiter = Program.TestAsync().GetAwaiter(); if (!awaiter.IsCompleted) { this.<>1__state = num = 0; this.<>u__1 = awaiter; Program.
d__0 stateMachine = this; this.<>t__builder.AwaitUnsafeOnCompletedd__0>(ref awaiter, ref stateMachine); return; } } else { awaiter = this.<>u__1; this.<>u__1 = new TaskAwaiter(); // 狀態機狀態從 0 流轉到 -1 this.<>1__state = num = -1; } // 結束對 TestAsync() 的等待 awaiter.GetResult(); // 執行延續方法 Console.Write(" World!"); } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } // 狀態機狀態從 -1 流轉到 -2 this.<>1__state = -2; // 設定非同步 Main 方法最終返回結果 this.<>t__builder.SetResult(); } ``` 至此,整個非同步方法的執行就結束了,通過一張圖總結一下: ![](https://img2020.cnblogs.com/blog/1010000/202101/1010000-20210120165259645-250717138.png) 最後,我們看一下各個執行緒的狀態,看看和你的推理是否一致(如果有不清楚的聯絡我,我會通過文字補充): ![](https://img2020.cnblogs.com/blog/1010000/202101/1010000-20210120165320220-2013763193.gif) ## 多個 async await 巢狀 理解了async await的簡單使用,那你可曾想過,如果有多個 async await 巢狀,那會出現什麼情況呢?接下來就改造一下我們的例子,來研究研究: ``` static Task TestAsync() { return Task.Run(async () => { // 增加了這行 await Task.Run(() => { Console.Write("Say: "); }); Console.Write("Hello"); }); } ``` 反編譯之後的程式碼,上面已經講解的我就不再重複貼了,主要看看`TestAsync()`就行了: ``` private static Task TestAsync() => Task.Run(delegate { <>c.<b__1_0>d stateMachine = new <>c.<b__1_0>d { <>t__builder = AsyncTaskMethodBuilder.Create(), <>4__this = this, <>1__state = -1 }; stateMachine.<>t__builder.Start<<>c.<b__1_0>d>(ref stateMachine); return stateMachine.<>t__builder.Task; }); ``` 哦!原來,async await 的巢狀也就是狀態機的巢狀,相信你通過上面的狀態機狀態流轉,也能夠梳理除真正的執行邏輯,那我們就只看一下執行緒狀態吧: ![](https://img2020.cnblogs.com/blog/1010000/202101/1010000-20210120165459091-2100640352.gif) 這也印證了我上面所說的:當子執行緒完成執行任務時,會被釋放,或回到執行緒池供其他執行緒使用。 ## 多個 async await 在同一方法中順序執行 又可曾想過,如果有多個 async await 在同一方法中順序執行,又會是何種景象呢?同樣,先來個例子: ``` static async Task Main(string[] args) { Console.WriteLine("Let's Go!"); await Test1Async(); await Test2Async(); Console.Write(" World!"); } static Task Test1Async() { return Task.Run(() => { Console.Write("Say: "); }); } static Task Test2Async() { return Task.Run(() => { Console.Write("Hello"); }); } ``` 直接看狀態機: ``` [CompilerGenerated] private sealed class
d__0 : IAsyncStateMachine { // Fields public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public string[] args; private TaskAwaiter <>u__1; // Methods private void MoveNext() { int num = this.<>1__state; try { TaskAwaiter awaiter; TaskAwaiter awaiter2; if (num != 0) { if (num == 1) { awaiter = this.<>u__1; this.<>u__1 = default(TaskAwaiter); this.<>1__state = -1; goto IL_D8; } Console.WriteLine("Let's Go!"); awaiter2 = Program.Test1Async().GetAwaiter(); if (!awaiter2.IsCompleted) { this.<>1__state = 0; this.<>u__1 = awaiter2; Program.
d__0
d__ = this; this.<>t__builder.AwaitUnsafeOnCompletedd__0>(ref awaiter2, ref
d__); return; } } else { awaiter2 = this.<>u__1; this.<>u__1 = default(TaskAwaiter); this.<>1__state = -1; } awaiter2.GetResult(); // 待 Test1Async 完成後,繼續執行 Test2Async awaiter = Program.Test2Async().GetAwaiter(); if (!awaiter.IsCompleted) { this.<>1__state = 1; this.<>u__1 = awaiter; Program.
d__0
d__ = this; this.<>t__builder.AwaitUnsafeOnCompletedd__0>(ref awaiter, ref
d__); return; } IL_D8: awaiter.GetResult(); Console.Write(" World!"); } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } } ``` 可見,就是一個狀態機狀態一直流轉就完事了。我們就看看執行緒狀態吧: ![](https://img2020.cnblogs.com/blog/1010000/202101/1010000-20210120165515998-311183181.gif) ## WPF中使用 async await 上面我們都是通過控制檯舉的例子,這是沒有任何`SynchronizationContext`的,但是WPF(Winform同理)就不同了,在UI執行緒中,它擁有屬於自己的`DispatcherSynchronizationContext`。 ``` private async void Button_Click(object sender, RoutedEventArgs e) { // UI 執行緒會一直保持 Running 狀態,不會導致 UI 假死 Show(Thread.CurrentThread); await TestAsync(); Show(Thread.CurrentThread); } private Task TestAsync() { return Task.Run(() => { Show(Thread.CurrentThread); }); } private static void Show(Thread thread) { MessageBox.Show($"{nameof(thread.ManagedThreadId)}: {thread.ManagedThreadId}" + $"\n{nameof(thread.ThreadState)}: {thread.ThreadState}"); } ``` 通過使用`DispatcherSynchronizationContext`執行延續方法,又回到了 UI 執行緒中 ![](https://img2020.cnblogs.com/blog/1010000/202101/1010000-20210120165534935-171511763.gif)