task async await後續

幾個月前寫過一篇關於task、async和await的帖子,給我感覺還是比較抽象複雜的,涉及到了線程、il代碼、編譯器、關鍵字等等,有興趣的朋友可以轉過去看看,https://www.cnblogs.com/adair-blog/p/13093376.html。有了上一篇task的基礎,那麼今天我這邊就來簡單聊一下ConfigureAwait背後的原理,首先聲明,只是個人讀源碼的一些理解,理解不對的,看官請指出,以下所有源碼都是基於.NET6.0。
 
Task,在聊ConfigureAwait之前,我們先簡單回顧下task、async、await。task依然還是那個task,很複雜的一個對象,表示異步操作,默認情況下以線程池的方式執行,也可以以線程的方式執行,看代碼
 
protected internal override void QueueTask(Task task)
        {
            TaskCreationOptions options = task.Options;
            if (Thread.IsThreadStartSupported && (options &  TaskCreationOptions.LongRunning) != 0)
            {
                new Thread(s_longRunningThreadWork)
                {
                    IsBackground = true,
                    Name = ".NET Long Running Task"
                }.UnsafeStart(task);
            }
            else
            {
                ThreadPool.UnsafeQueueUserWorkItemInternal(task, (options &  TaskCreationOptions.PreferFairness) == 0);
            }
        }

 

如上代碼,最終task調用TaskScheduler調度器的QueueTask方法完成任務的調度,創建線程或者以線程池的方式執行,task有3種調度器,這只是其中一種。
Async,方法修飾符,關鍵字,表示該方法可能會有異步操作,與普通方法別無二致,async關鍵字主要是提供給編譯器編譯用的,看代碼
       
private static async Task Test()
        {
            Console.WriteLine(2);
            var v1 = await b();
            var v = await a();
            Console.WriteLine(6);
        }
        private static async Task<string> b()
        {
            Thread.Sleep(2000);
            return "3";
        }
        private static async Task<string> a()
        {
            Console.WriteLine(3);
            await Task.Run(() =>
            {
                Thread.Sleep(10000);
                Console.WriteLine(4);
            });
            Console.WriteLine(5);
            return "3";
        }
        static void Main(string[] args)
        {
            Console.WriteLine(1);
            var v = Test();
            Console.WriteLine(7);
            Console.Read();
        }    

 

以上就是我的測試代碼,隨便寫的,此時我們通過vs編譯器編譯生成,看看編譯器對它幹了啥,看圖
 
 
 
我們的async方法被編譯成了一個個的類class,不過這些類class有些特別,繼承了IAsyncStateMachine接口,同時提供了三個方法,構造器、movenext、setstatemachine,後面這兩方法是實現了IAsyncStateMachine接口,IAsyncStateMachine就是大家說的狀態機,有了狀態機,狀態何來?接着看。
 
await,await必須要在async修飾的方法裏面聲明,這也就很好理解了,async提供狀態機,await提供狀態,我是這麼理解的,不知道對不對哈。await同樣是關鍵字,提供給編譯器編譯用的,接着以上代碼,我們簡單看下編譯器針對await關鍵字的編譯行爲,看代碼
 
有啓動線程的il代碼(a_class)
IL_0054:  dup
    IL_0055:  stsfld     class [System.Runtime]System.Action  ConsoleApp5.Program/'<>c'::'<>9__4_0'
    IL_005a:  call       class [System.Runtime]System.Threading.Tasks.Task  [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
    IL_005f:  callvirt   instance valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter  [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
    IL_0064:  stloc.3
    IL_0065:  ldloca.s   V_3
    IL_0067:  call       instance bool  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
    IL_006c:  brtrue.s   IL_00af // 如果爲true也就是任務跑完了,直接跳轉指令到獲取返回值,那部分代碼我沒貼了
    IL_006e:  ldarg.0
    IL_006f:  ldc.i4.0
    IL_0070:  dup
    IL_0071:  stloc.0
    IL_0072:  stfld      int32 ConsoleApp5.Program/'<a>d__4'::'<>1__state'
    IL_0077:  ldarg.0
    IL_0078:  ldloc.3
    IL_0079:  stfld      valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter  ConsoleApp5.Program/'<a>d__4'::'<>u__1'
    IL_007e:  ldarg.0
    IL_007f:  stloc.s    V_4
    IL_0081:  ldarg.0
    IL_0082:  ldflda     valuetype  [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>  ConsoleApp5.Program/'<a>d__4'::'<>t__builder'
    IL_0087:  ldloca.s   V_3
    IL_0089:  ldloca.s   V_4
    IL_008b:  call       instance void valuetype  [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::AwaitUnsafeOnCompleted<valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter,class  ConsoleApp5.Program/'<a>d__4'>(!!0&, // 開始跑任務狀態
 
未啓動線程的il代碼(b_class)
IL_0025:  call       instance string [System.Runtime]System.Int32::ToString()
    IL_002a:  call       string [System.Runtime]System.String::Concat(string,
                                                                      string)
    IL_002f:  call       void [System.Console]System.Console::WriteLine(string)
    IL_0034:  nop
    IL_0035:  ldstr      "3"
    IL_003a:  stloc.1
    IL_003b:  leave.s    IL_0055 // 執行方法體裏面的代碼,直接執行leave指令,跳轉到指定地址
 
test方法的調用棧 
IL_00aa:  stfld      string ConsoleApp5.Program/'<Test>d__0'::'<>s__6'
    IL_00af:  call       class [System.Runtime]System.Threading.Tasks.Task`1<string>  ConsoleApp5.Program::b() // 創建b對象
    IL_00b4:  callvirt   instance valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class  [System.Runtime]System.Threading.Tasks.Task`1<string>::GetAwaiter()
    IL_00b9:  stloc.3
    IL_00ba:  ldloca.s   V_3
    IL_00bc:  call       instance bool valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<string>::get_IsCompleted() // 判斷任務是否完成,這裏是true
    IL_00c1:  brtrue.s   IL_0106 // 直接跳轉獲取result

 

以上代碼我貼了三個class的部分il代碼,a、b和test,也做了簡單註釋,a類裏面聲明瞭await關鍵字,並且開啓了新的線程,b類裏面沒有聲明await關鍵字,大家可以簡單看下il代碼,這裏面有幾個小地方需要注意,1. await等待對象如果沒有開啓線程,那麼它還是同步方式執行的,如果在沒有開啓新線程的情況下,你還堅持聲明async、await關鍵字,我只能說,會影響性能。2. await等待對象裏面如果沒有開啓新線程,那麼它阻塞是的調用線程,如果有開啓線程,那麼它阻塞的是“當前”方法調用棧。簡單總結一下,以var v1 = await b();這行代碼爲例,這行代碼所在的方法會被編譯成class,b方法同樣也被編譯成了b類class,同時實現了三個方法構造器和movenext,movenext方法體裏面的內容就是我們定義的b方法體的內容,調用線程先調用了被編譯器編譯成class的b的構造器,在b的構造器裏面,調用了movenext函數,因爲b的方法體裏面沒有開啓新的線程,調用線程會執行完b的方法體,再return出來,獲取awaiter對象,接着判斷這個movenext函數是否執行完畢,如果執行完畢,直接獲取result,調用線程接着執行下面的邏輯,如果b方法裏面開啓了新的線程,背後邏輯會有區別。task、async、await回顧就到這吧。
 
ConfigureAwait是什麼?其實它就是一個函數啊,說的有點廢話,它能幹嘛?防止死鎖,提高性能,那麼我就好奇了,它是怎麼做到的,懷着好奇心,我從github上down了源代碼,github地址,https://github.com/dotnet/runtime,runtime?沒錯,.NET基礎庫都在這裏,包括clr。源碼一打開,把我驚呆了,這個函數的實現就一行代碼,直接return一個ConfiguredTaskAwaitable對象,看代碼,
 
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
        {
            return new ConfiguredTaskAwaitable(this, continueOnCapturedContext);
        }
 
如上就是ConfigureAwait方法的實現,看到這裏,我想朋友們跟我一樣也有很多疑惑,玄機應該就在 ConfiguredTaskAwaitable這個對象裏面,看下它的定義。
 
public readonly struct ConfiguredTaskAwaitable
        {        
            private readonly ConfiguredTaskAwaitable.ConfiguredTaskAwaiter  m_configuredTaskAwaiter;
            public ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter GetAwaiter()
            {
                return m_configuredTaskAwaiter;
            }
            ...
        }
 
其他成員代碼我就不貼了,這裏我們只需要關注的就是getAwaiter方法,以及它的返回值ConfiguredTaskAwaiter,繼續看下它的定義。
 
public readonly struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion,  IConfiguredTaskAwaiter
        {
            internal readonly Task m_task;
            internal ConfiguredTaskAwaiter(Task task, bool continueOnCapturedContext)
            {
                Debug.Assert(task != null, "Constructing an awaiter requires a task to  await.");
                m_task = task;
                m_continueOnCapturedContext = continueOnCapturedContext;
            }
            public bool IsCompleted => m_task.IsCompleted;
            public void OnCompleted(Action continuation)
            {
                TaskAwaiter.OnCompletedInternal(m_task, continuation,  m_continueOnCapturedContext, flowExecutionContext: true);
            }
 
            public void UnsafeOnCompleted(Action continuation)
            {
                TaskAwaiter.OnCompletedInternal(m_task, continuation,  m_continueOnCapturedContext, flowExecutionContext: false);
            }
            
            public void GetResult()
            {
                TaskAwaiter.ValidateEnd(m_task);
            }
        }

  

以上就是ConfiguredTaskAwaiter的全貌,這裏簡單理一下awaiter成員函數的作用以及背後調用邏輯(討論的是編譯器行爲,因爲是await表達式),以代碼var vd = await d().ConfigureAwait(false);爲例,1. 調用GetAwaiter()方法獲取ConfiguredTaskAwaiter對象,2. 隨後判斷awaiter對象的get_IsCompleted是否爲true?(此處如果d方法裏面未開啓新的線程,這裏就會返回true,反之就會返回false),3. 如果爲true則調用上面的GetResult()方法,獲取結果,如果爲false,則調用AwaitUnsafeOnCompleted()方法,Hook通知,待任務完成後調用上面的UnsafeOnCompleted()方法,重新回到狀態機movenext,調用GetResult()方法獲取結果,4. 接着執行後續邏輯。awaiter不是隻有ConfiguredTaskAwaiter對象,還有TaskAwaiter等待,默認情況下如await d();返回的就是TaskAwaiter對象,有興趣的朋友也可以實現自己的awaiter對象,只要符合以上able和awaiter約定就行,這個就不展開了,要實現一個成熟的庫還是比較複雜的。
 
ConfiguredTaskAwaiter對象的所有方法都介紹了,也介紹了背後調用邏輯,接下來繼續探索它能解決死鎖問題,其實邏輯已經很清晰了,那就是hook到任務完成之後的執行邏輯,我們需要跟蹤的就是AwaitUnsafeOnCompleted()方法,看代碼。
 
internal static void AwaitUnsafeOnCompleted<TAwaiter>(
            ref TAwaiter awaiter, IAsyncStateMachineBox box)
            where TAwaiter : ICriticalNotifyCompletion
        {
            if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
            {
                ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter); //  relies on TaskAwaiter/TaskAwaiter<T> having the same layout
                TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box,  continueOnCapturedContext: true);
            }
            else if ((null != (object?)default(TAwaiter)) && (awaiter is  IConfiguredTaskAwaiter))
            {
                ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref  Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(ref awaiter);
                TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box,  ta.m_continueOnCapturedContext);
            }
            ...
        }

  

如上代碼,我們繼續跟蹤UnsafeOnCompletedInternal方法,
if (continueOnCapturedContext) // 這個就是最開始傳入的false,默認爲true
            {
                SynchronizationContext? syncCtx = SynchronizationContext.Current;  // 獲取SynchronizationContext同步上下文,aspnetcore已經取消了這個對象
                if (syncCtx != null && syncCtx.GetType() !=  typeof(SynchronizationContext))
                {
                    var tc = new SynchronizationContextAwaitTaskContinuation(syncCtx,  stateMachineBox.MoveNextAction, flowExecutionContext: false);
                    if (!AddTaskContinuation(tc, addBeforeOthers: false))
                    {
                        tc.Run(this, canInlineContinuationTask: false); // 最終通過c.m_syncContext.Post(s_postCallback, c.m_action); Post callback
                    }
                    return;
                }
                else
                {
                    TaskScheduler? scheduler = TaskScheduler.InternalCurrent;
                    if (scheduler != null && scheduler != TaskScheduler.Default)
                    {
                        var tc = new TaskSchedulerAwaitTaskContinuation(scheduler,  stateMachineBox.MoveNextAction, flowExecutionContext: false);
                        if (!AddTaskContinuation(tc, addBeforeOthers: false))
                        {
                            tc.Run(this, canInlineContinuationTask: false);
                        }
                        return;
                    }
                }
            } 
            
            if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false))  // 我們顯示指定false,所以會走這個邏輯。
            {
                ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, preferLocal:  true);
            }

  

以上代碼做了簡單的註釋,最後我個人的理解就是,在aspnetcore環境下沒有死鎖的風險,但是在GUI環境下面,需要注意死鎖和UI線程上下文環境,好了就到這吧。
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章