• [C#]async和await刨根问底


    上一篇随笔留下了几个问题没能解决:
    · 调用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后操作),总之就是麻烦死了。。。

  • 相关阅读:
    linux下mysql区分大小写的内容
    jar包 pom
    项目的考虑
    webservice
    MySQL外键设置中的的 Cascade、NO ACTION、Restrict、SET NULL
    JVM参数最佳实践:元空间的初始大小和最大大小
    JVM问题排查工具:Serviceability-Agent介绍
    Spring Boot 2.x基础教程:构建RESTful API与单元测试
    彻底搞懂JVM类加载器:基本概念
    如何解决90%的问题?10位阿里大牛公布方法
  • 原文地址:https://www.cnblogs.com/vd630/p/4596203.html
Copyright © 2020-2023  润新知