task async await后续


几个月前写过一篇关于task、async和await的帖子,给我感觉还是比较抽象复杂的,涉及到了线程、il代码、编译器、关键字等等,有兴趣的朋友可以转过去看看,。有了上一篇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 b()
        {
            Thread.Sleep(2000);
            return "3";
        }
        private static async Task 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/'d__4'::'<>1__state'
    IL_0077:  ldarg.0
    IL_0078:  ldloc.3
    IL_0079:  stfld      valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter  ConsoleApp5.Program/'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  ConsoleApp5.Program/'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::AwaitUnsafeOnCompletedd__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/'d__0'::'<>s__6'
    IL_00af:  call       class [System.Runtime]System.Threading.Tasks.Task`1  ConsoleApp5.Program::b() // 创建b对象
    IL_00b4:  callvirt   instance valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1 class  [System.Runtime]System.Threading.Tasks.Task`1::GetAwaiter()
    IL_00b9:  stloc.3
    IL_00ba:  ldloca.s   V_3
    IL_00bc:  call       instance bool valuetype  [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1::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.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(
            ref TAwaiter awaiter, IAsyncStateMachineBox box)
            where TAwaiter : ICriticalNotifyCompletion
        {
            if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
            {
                ref TaskAwaiter ta = ref Unsafe.As(ref awaiter); //  relies on TaskAwaiter/TaskAwaiter 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(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线程上下文环境,好了就到这吧。