[C#]async和await刨根問底

上一篇隨筆留下了幾個問題沒能解決:
· 調用IAsyncStateMachine.MoveNext方法的線程什麼時候發起的?
· lambda的執行爲什麼先於MoveNext方法?
· 後執行的MoveNext方法作了些什麼事情?html

那麼今天就來嘗試解決它們吧~
PS: 本文中部分代碼來自上一篇隨筆,具體來源可參考註釋中的章節標題

async

1、哪裏來的線程ide

經過上一篇隨筆的調查咱們知道了,async標記的方法的方法體會被編譯到一個內部結構體的MoveNext方法中,而且也找到了MoveNext的調用者,再且也證明了有兩個調用者是來自於主線程以外的同一個工做線程。
但是這一個線程是什麼時候發起的呢?上一次調查時沒能找到答案,這一次就繼續從MoveNext方法開始,先找找看Task相關的操做有哪些。ui

 1 // 3、理解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裏面作了些什麼吧。this

 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。
spa

先來看看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調用MoveNextcode

第40行的賦值不影響Action中的Run,只是在頭尾追加了狀態記錄的操做。
接下來就趕忙找一找執行這個Action的地方吧!
深刻UnsafeOnCompleted方法,最終能夠找到以下的方法,第一個參數就是要跟蹤的對象:htm

 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

到此爲止算是搞定第一個問題了。

2、lambda爲什麼先行

先來回憶一下GetHere方法的內容:

// 3、理解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幾個關鍵點便可以驗證這一順序。第二個也算搞定了。

3、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後的代碼。

4、水落石出

async和await的輪廓逐漸清晰了~再結合上一篇的一段代碼來看看:

// 2、理解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()
}

黃底的兩句代碼本來是在哪的還記得嗎?看這裏:

// 2、理解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移交給線程池ThreadPoolGlobal4. 子線程執行完主線程遞交來的Task後,再次走入MoveNext方法,執行await後的代碼最後想說的是:這一陣在辦公積金銷戶提取,整個過程就像是個async方法,把申請提交給管理中心(await前操做)之後就得開始等待(await)他們對申請進行審覈(執行Task),這個過程加上週末得整整五天,以後還得去管理中心取款(await後操做),總之就是麻煩死了。。。

相關文章
相關標籤/搜索