上一篇隨筆留下了幾個問題沒能解決:
· 調用IAsyncStateMachine.MoveNext方法的線程何時發起的?
· lambda的執行為何先於MoveNext方法?
· 後執行的MoveNext方法做了些什麼事情?
那麼今天就來嘗試解決它們吧~
PS: 本文中部分代碼來自上一篇隨筆,具體來源可參考注釋中的章節標題
一、哪裡來的線程?
通過上一篇隨筆的調查我們知道了,async標記的方法的方法體會被編譯到一個內部結構體的MoveNext方法中,並且也找到了MoveNext的調用者,再且也證實了有兩個調用者是來自於主線程之外的同一個工作線程。
可是這一個線程是何時發起的呢?上一次調查時沒能找到答案,這一次就繼續從MoveNext方法開始,先找找看Task相關的操作有哪些。
1 // 三、理解await
2 bool '<>t__doFinallyBodies';
3 Exception '<>t__ex';
4 int CS$0$0000;
5 TaskAwaiter<string> CS$0$0001;
6 TaskAwaiter<string> CS$0$0002;
7
8 try
9 {
10 '<>t__doFinallyBodies' = true;
11 CS$0$0000 = this.'<>1__state';
12 if (CS$0$0000 != 0)
13 {
14 CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
15 if (!CS$0$0001.IsCompleted)
16 {
17 this.'<>1__state' = 0;
18 this.'<>u__$awaiter1' = CS$0$0001;
19 this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
20 '<>t__doFinallyBodies' = false;
21 return;
22 }
23 }
24 else
25 {
26 CS$0$0001 = this.'<>u__$awaiter1';
27 this.'<>u__$awaiter1' = CS$0$0002;
28 this.'<>1__state' = -1;
29 }
30
31 Console.WriteLine(CS$0$0001.GetResult());
32 }
注意到14行的GetHere方法返回了一個Task<string>,隨後的GetAwaiter返回的是TaskAwaiter<string>。
不過這兩個Get方法都沒有做什麼特別的處理,那麼就看看接下來是誰使用了TaskAwaiter<string>實例。
於是就來看看19行的AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted裡面做了些什麼吧。
1 // System.Runtime.CompilerServices.AsyncVoidMethodBuilder
2 [__DynamicallyInvokable, SecuritySafeCritical]
3 public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
4 ref TAwaiter awaiter, ref TStateMachine stateMachine)
5 where TAwaiter : ICriticalNotifyCompletion
6 where TStateMachine : IAsyncStateMachine
7 {
8 try
9 {
10 Action completionAction = this.m_coreState
11 .GetCompletionAction<AsyncVoidMethodBuilder, TStateMachine>(ref this, ref stateMachine);
12 awaiter.UnsafeOnCompleted(completionAction);
13 }
14 catch (Exception exception)
15 {
16 AsyncMethodBuilderCore.ThrowAsync(exception, null);
17 }
18 }
這裡主要做了兩件事:
一是創建了一個Action,MoveNext方法的信息已經隨著stateMachine被封裝進去了。
二是把上面這個Action交給Awaiter,讓它在await的操作完成後執行這個Action。
先來看看Action的構建細節吧:
1 // System.Runtime.CompilerServices.AsyncMethodBuilderCore
2 [SecuritySafeCritical]
3 internal Action GetCompletionAction<TMethodBuilder, TStateMachine>(ref TMethodBuilder builder, ref TStateMachine stateMachine)
4 where TMethodBuilder : IAsyncMethodBuilder
5 where TStateMachine : IAsyncStateMachine
6 {
7 Debugger.NotifyOfCrossThreadDependency();
8 ExecutionContext executionContext = ExecutionContext.FastCapture();
9 Action action;
10 AsyncMethodBuilderCore.MoveNextRunner moveNextRunner;
11 if (executionContext != null && executionContext.IsPreAllocatedDefault)
12 {
13 action = this.m_defaultContextAction;
14 if (action != null)
15 {
16 return action;
17 }
18 moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext);
19 action = new Action(moveNextRunner.Run);
20 if (AsyncCausalityTracer.LoggingOn)
21 {
22 action = (this.m_defaultContextAction = this.OutputAsyncCausalityEvents<TMethodBuilder>(ref builder, action));
23 }
24 else
25 {
26 this.m_defaultContextAction = action;
27 }
28 }
29 else
30 {
31 moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext);
32 action = new Action(moveNextRunner.Run);
33 if (AsyncCausalityTracer.LoggingOn)
34 {
35 action = this.OutputAsyncCausalityEvents<TMethodBuilder>(ref builder, action);
36 }
37 }
38 if (this.m_stateMachine == null)
39 {
40 builder.PreBoxInitialization<TStateMachine>(ref stateMachine);
41 this.m_stateMachine = stateMachine;
42 this.m_stateMachine.SetStateMachine(this.m_stateMachine);
43 }
44 moveNextRunner.m_stateMachine = this.m_stateMachine;
45 return action;
46 }
這段的分支有點多,行號上的標記是我DEBUG時經過的分支。
可以看到,這個方法裡面出現了MoveNext方法的調用者MoveNextRunner,它的Run方法被封裝到了返回的Action裡。
也就是說,只要這個Action被執行,就會進入Run方法,而Run方法裡面有兩條分支,簡單來說就是:
1.直接調用MoveNext
2.通過InvokeMoveNext調用MoveNext
第40行的賦值不影響Action中的Run,只是在頭尾追加了狀態記錄的操作。
接下來就趕緊找一找執行這個Action的地方吧!
深入UnsafeOnCompleted方法,最終可以找到如下的方法,第一個參數就是要跟蹤的對象:
1 // System.Threading.Tasks.Task
2 [SecurityCritical]
3 internal void SetContinuationForAwait(
4 Action continuationAction,
5 bool continueOnCapturedContext,
6 bool flowExecutionContext,
7 ref StackCrawlMark stackMark)
8 {
9 TaskContinuation taskContinuation = null;
10 if (continueOnCapturedContext)
11 {
12 SynchronizationContext currentNoFlow = SynchronizationContext.CurrentNoFlow;
13 if (currentNoFlow != null && currentNoFlow.GetType() != typeof(SynchronizationContext))
14 {
15 taskContinuation = new SynchronizationContextAwaitTaskContinuation(
16 currentNoFlow, continuationAction, flowExecutionContext, ref stackMark);
17 }
18 else
19 {
20 TaskScheduler internalCurrent = TaskScheduler.InternalCurrent;
21 if (internalCurrent != null && internalCurrent != TaskScheduler.Default)
22 {
23 taskContinuation = new TaskSchedulerAwaitTaskContinuation(
24 internalCurrent, continuationAction, flowExecutionContext, ref stackMark);
25 }
26 }
27 }
28 if (taskContinuation == null && flowExecutionContext)
29 {
30 taskContinuation = new AwaitTaskContinuation(continuationAction, true, ref stackMark);
31 }
32 if (taskContinuation != null)
33 {
34 if (!this.AddTaskContinuation(taskContinuation, false))
35 {
36 taskContinuation.Run(this, false);
37 return;
38 }
39 }
40 else if (!this.AddTaskContinuation(continuationAction, false))
41 {
42 AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this);
43 }
44 }
同樣的,行號的標記意味著經過的分支。繼續跟進:
1 // System.Threading.Tasks.AwaitTaskContinuation
2 [SecurityCritical]
3 internal static void UnsafeScheduleAction(Action action, Task task)
4 {
5 AwaitTaskContinuation awaitTaskContinuation = new AwaitTaskContinuation(action, false);
6 TplEtwProvider log = TplEtwProvider.Log;
7 if (log.IsEnabled() && task != null)
8 {
9 awaitTaskContinuation.m_continuationId = Task.NewId();
10 log.AwaitTaskContinuationScheduled(
11 (task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id,
12 task.Id,
13 awaitTaskContinuation.m_continuationId);
14 }
15 ThreadPool.UnsafeQueueCustomWorkItem(awaitTaskContinuation, false);
16 }
1 // System.Threading.ThreadPool
2 [SecurityCritical]
3 internal static void UnsafeQueueCustomWorkItem(IThreadPoolWorkItem workItem, bool forceGlobal)
4 {
5 ThreadPool.EnsureVMInitialized();
6 try
7 {
8 }
9 finally
10 {
11 ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal);
12 }
13 }
這裡出現了全局線程池,然而沒有找到MSDN對ThreadPoolGlobals的解釋,這裡頭的代碼又實在太多了。。。暫且模擬一下看看:
1 Console.WriteLine("HERE");
2 var callback = new WaitCallback(state => Println("From ThreadPool"));
3 ThreadPool.QueueUserWorkItem(callback);
4 Console.WriteLine("THERE");
QueueUserWorkItem方法內部調用了ThreadPoolGlobals.workQueue.Enqueue,運行起來效果是這樣的:
HERE THERE From ThreadPool
再看看線程信息:
Function: CsConsole.Program.Main(), Thread: 0x2E58 主線程 Function: CsConsole.Program.Main(), Thread: 0x2E58 主線程 Function: CsConsole.Program.Main.AnonymousMethod__6(object), Thread: 0x30EC 工作線程
和async的表現簡直一模一樣是不是~?從調用堆棧也可以看到lambda的執行是源於這個workQueue:

到此為止算是搞定第一個問題了。
二、lambda為何先行?
先來回憶一下GetHere方法的內容:
// 三、理解await
Task<string> GetHere()
{
return Task.Run(() =>
{
Thread.Sleep(1000);
return "HERE";
});
}
要追蹤的lambda就是在這裡構造的,而調用GetHere的地方也只有一個,就是MoveNext方法的try塊。
而MoveNext的調用方也都找出來了:

其中Start方法是在主線程中調用的,可以由SampleMethod追溯到。那麼以下的調用信息:
Function: Test.Program.Main(string[]), Thread: 0xE88 主線程 Function: Test.Program.GetHere.AnonymousMethod__3(), Thread: 0x37DC 工作線程 Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run(), Thread: 0x37DC 工作線程 Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x37DC 工作線程
這個順序不是有點奇怪嗎?lambda怎麼能先於MoveNextRunner的兩個方法執行?
其實我在這裡犯了一個很明顯的思維錯誤。。。Start調用來自主線程,lambda調用來自子線程,於是直覺性地否定了它們之間的關聯。。。
很顯然,整個過程其實應該是這樣的:
1. 主線程:Start方法調用了MoveNext,MoveNext調用了GetHere
2. 主線程:GetHere方法返回了包含lambda信息的Task
3. 主線程:Task經過變換與包裝,最終進入了線程池
4. 子線程:通過Task調用了lambda
5. 子線程:通過Runner調用了MoveNext
子線程中的lambda是來源於主線程第一次調用的MoveNext,和之後的Run啊InvokeMoveNext是沒有關系的,所以這個順序也就不奇怪了。
通過DEBUG幾個關鍵點即可以驗證這一順序。第二個也算搞定了。
三、MoveNext干了什麼?
第二個問題雖然解決了,但是也讓第三個問題顯得更加重要,既然lambda確實是先於MoveNext,那麼MoveNext到底做了些什麼?
通過之前的調查,現在知道了:
1. MoveNext在lambda執行之前被Start方法在主線程調用了一次,過程中把lambda封送給了線程池
2. MoveNext在lambda執行之後被InvokeMoveNext又調用了一次,這一次做了什麼處理是尚不明了的
回頭看本文的第一段代碼,前後兩次進入同一段代碼,但是做了不同的事情,那麼顯然就是兩次走了不同的分支咯。
由於這段代碼本身是DEBUG不進去的,所以只能在其內部調用的方法裡斷點了。我打了如下幾個斷點:
· Task<TResult>.GetAwaiter
· AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted
· TaskAwaiter<TResult>.GetResult
· Program.SampleMethod
· MoveNextRunner.InvokeMoveNext
來看看執行結果如何吧:
Function: Test.Program.SampleMethod(), Thread: 0x9BC 主線程 Function: System.Threading.Tasks.Task<TResult>.GetAwaiter(), Thread: 0x9BC 主線程 Function: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted<TAwaiter,TStateMachine>(ref TAwaiter, ref TStateMachine), Thread: 0x9BC 主線程 Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x3614 工作線程 Function: System.Runtime.CompilerServices.TaskAwaiter<TResult>.GetResult(), Thread: 0x3614 工作線程
需要注意的是,斷到InvokeMoveNext裡頭的時候,只有這一行代碼:
((IAsyncStateMachine)stateMachine).MoveNext();
而當我按下F11步入之後,可以猜一猜跳到了哪:
async void SampleMethod()
{
Console.WriteLine(await GetHere());
}
而在這個時候GetResult還沒執行到。
由此可以整理出try塊裡的執行過程如下:
1 try
2 {
3 '<>t__doFinallyBodies' = true;
4 CS$0$0000 = this.'<>1__state';
5 if (CS$0$0000 != 0)
6 {
7 CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
8 if (!CS$0$0001.IsCompleted)
9 {
10 this.'<>1__state' = 0;
11 this.'<>u__$awaiter1' = CS$0$0001;
12 this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
13 '<>t__doFinallyBodies' = false;
14 return;
15 }
16 }
17 else
18 {
19 CS$0$0001 = this.'<>u__$awaiter1';
20 this.'<>u__$awaiter1' = CS$0$0002;
21 this.'<>1__state' = -1;
22 }
23
24 Console.WriteLine(CS$0$0001.GetResult());
25 }
紅字是第一次經過的分支,黃底是第二次經過的分支。
而前面說到的F11進入的區塊,實際上就是這裡的第24行。
所以現在可以知道,第二次MoveNext做了什麼:
執行async方法中await後的代碼。
四、水落石出
async和await的輪廓逐漸清晰了~再結合上一篇的一段代碼來看看:
// 二、理解async
void MoveNext()
{
bool local0;
Exception local1;
try
{
local0 = true;
Thread.Sleep(1000);
Console.WriteLine("HERE");
}
catch (Exception e)
{
local1 = e;
this.'<>1__state' = -2;
this.'<>t__builder'.SetException(local1);
return;
}
this.'<>1__state' = -2;
this.'<>t__builder'.SetResult()
}
黃底的兩句代碼原本是在哪的還記得嗎?看這裡:
// 二、理解async
async void SampleMethod()
{
Thread.Sleep(1000);
Console.WriteLine("HERE");
}
因為這個async方法中沒有出現await調用,所以可以認為僅有的兩句代碼是出現在await操作之前。
再讓SampleMethod變成這樣:
async void SampleMethod()
{
Console.WriteLine("WHERE");
Console.WriteLine(await GetHere());
}
再看看現在的MoveNext方法:
1 try
2 {
3 '<>t__doFinallyBodies' = true;
4 CS$0$0000 = this.'<>1__state';
5 if (CS$0$0000 != 0)
6 {
7 Console.WriteLine("WHERE");
8 CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
9 if (!CS$0$0001.IsCompleted)
10 {
11 this.'<>1__state' = 0;
12 this.'<>u__$awaiter1' = CS$0$0001;
13 this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
14 '<>t__doFinallyBodies' = false;
15 return;
16 }
17 }
18 else
19 {
20 CS$0$0001 = this.'<>u__$awaiter1';
21 this.'<>u__$awaiter1' = CS$0$0002;
22 this.'<>1__state' = -1;
23 }
24
25 Console.WriteLine(CS$0$0001.GetResult());
26 }
這樣就可以很明顯的看出來await前後的代碼被放到了兩個區塊裡,而這兩個區塊,也就是之前看到的兩次執行MoveNext走過的分支。
最終調查結果如下:
1. async方法中的代碼會被移交給IAsyncStateMachine的MoveNext方法
2. async方法中await操作前後的代碼被分離
3. 主線程直接執行await前的代碼,並將await的Task移交給線程池ThreadPoolGlobal
4. 子線程執行完主線程遞交來的Task後,再次走入MoveNext方法,執行await後的代碼
最後想說的是:
這一陣在辦公積金銷戶提取,整個過程就像是個async方法,把申請提交給管理中心(await前操作)以後就得開始等待(await)他們對申請進行審核(執行Task),這個過程加上周末得整整五天,之後還得去管理中心取款(await後操作),總之就是麻煩死了。。。