前言
一個老掉牙的話題,園子裡的相關優秀文章已經有很多了,我寫這篇文章完全是想以自己的思維方式來談一談自己的理解。(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 <Main>(string[] args)
{
Main(args).GetAwaiter().GetResult();
}
[AsyncStateMachine(typeof(<Main>d__0)), DebuggerStepThrough]
private static Task Main(string[] args)
{
<Main>d__0 stateMachine = new <Main>d__0
{
<>t__builder = AsyncTaskMethodBuilder.Create(),
args = args,
<>1__state = -1
};
stateMachine.<>t__builder.Start<<Main>d__0>(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
// 實現了 IAsyncStateMachine 介面
[CompilerGenerated]
private sealed class <Main>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方法內部實現,它主要做了三件事:
- 首先,建立了一個型別為
<Main>d__0
的狀態機 stateMachine,並初始化了公共變數 <>t__builder、args、<>1__state = -1- <>t__builder:負責非同步相關的操作,是實現非同步 Main 方法非同步的核心
- <>1__state:狀態機的當前狀態
- 然後,呼叫
Start
方法,藉助 stateMachine, 來執行我們在非同步 Main 方法中寫的程式碼 - 最後,將指示非同步 Main 方法執行狀態的
Task
物件返回出去
Start
首先,我們先來看一下Start
的內部實現
// 所屬結構體:AsyncTaskMethodBuilder
[SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable]
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();
}
}
我猜,你只能看懂stateMachine.MoveNext()
,對不對?好,那我們就來看看這個狀態機類<Main>d__0
,並且著重看它的方法MoveNext
。
MoveNext
[CompilerGenerated]
private sealed class <Main>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.<Main>d__0 stateMachine = this;
// 配置 TestAsync() 完成後的延續
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__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)
{
}
}
先簡單理一下內部邏輯:
- 設定變數 num = -1,此時 num != 0,則會進入第一個if語句,
- 首先,執行
Console.WriteLine("Let's Go!")
- 然後,呼叫非同步方法
TestAsync
,TestAsync
方法會在另一個執行緒池執行緒中執行,並獲取指示該方法執行狀態的 awaiter - 如果此時
TestAsync
方法已執行完畢,則像沒有非同步一般:- 繼續執行接下來的
Console.Write(" World!")
- 最後設定 <>1__state = -2,並設定非同步 Main 方法的返回結果
- 繼續執行接下來的
- 如果此時
TestAsync
方法未執行完畢,則:- 設定 <>1__state = num = 0
- 呼叫
AwaitUnsafeOnCompleted
方法,用於配置當TestAsync
方法完成時的延續,即Console.Write(" World!")
- 返回指示非同步 Main 方法執行狀態的 Task 物件,由於同步 Main 方法中通過使用
GetResult()
同步阻塞主執行緒等待任務結束,所以不會釋放主執行緒(廢話,如果釋放了程式就退出了)。不過對於其他子執行緒,一般會釋放該執行緒
大部分邏輯我們都可以很容易的理解,唯一需要深入研究的就是AwaitUnsafeOnCompleted
,那我們接下來就看看它的內部實現
AwaitUnsafeOnCompleted
// 所屬結構體:AsyncTaskMethodBuilder
[__DynamicallyInvokable]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: ICriticalNotifyCompletion where TStateMachine: IAsyncStateMachine
{
this.m_builder.AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref awaiter, ref stateMachine);
}
// 所屬結構體:AsyncTaskMethodBuilder<TResult>
[SecuritySafeCritical, __DynamicallyInvokable]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(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<TResult> 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》了
接著來到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.<Main>d__0 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__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();
}
至此,整個非同步方法的執行就結束了,通過一張圖總結一下:
最後,我們看一下各個執行緒的狀態,看看和你的推理是否一致(如果有不清楚的聯絡我,我會通過文字補充):
多個 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.<<TestAsync>b__1_0>d stateMachine = new <>c.<<TestAsync>b__1_0>d {
<>t__builder = AsyncTaskMethodBuilder.Create(),
<>4__this = this,
<>1__state = -1
};
stateMachine.<>t__builder.Start<<>c.<<TestAsync>b__1_0>d>(ref stateMachine);
return stateMachine.<>t__builder.Task;
});
哦!原來,async await 的巢狀也就是狀態機的巢狀,相信你通過上面的狀態機狀態流轉,也能夠梳理除真正的執行邏輯,那我們就只看一下執行緒狀態吧:
這也印證了我上面所說的:當子執行緒完成執行任務時,會被釋放,或回到執行緒池供其他執行緒使用。
多個 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 <Main>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.<Main>d__0 <Main>d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter2, ref <Main>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.<Main>d__0 <Main>d__ = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter, ref <Main>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)
{
}
}
可見,就是一個狀態機狀態一直流轉就完事了。我們就看看執行緒狀態吧:
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 執行緒中