【C# TAP 异步编程】三、async\await的运作机理详解 【2022年原创】


【原创】 本文只是个人笔记,很多错误,欢迎指出。

环境:vs2022  .net6.0 C#10

async\await的代码运行顺序

通过分析一段代码运行来了解async\await的运作机理(本案例是控制台应用程序)

namespace MyTask;
class Program
{
    public static void Main(string[] args)
    {
        Task<int> baconTask = GetUrlContentLengthAsync(3);
        baconTask.ContinueWith(t => Console.WriteLine(t.Result));
        Console.Read();
    }
    static async Task<int> GetUrlContentLengthAsync(int slices)
    {
        HttpClient httpClient = new();

        //********* 也可以合并起来写*********//
       // string content = await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");
//将生成TaskAwaiter的等待器,该等待器继承 ITaskAwaiter Task<string> getContent= httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); string content = await getContent; //await getContent反编译后是awaiter = 5__2.GetAwaiter(); DoInDependentWork(); return content.Length; } static void DoInDependentWork() { Console.WriteLine($"content.Length:Working"); } }

备用 改进后的代码,输出线程的变化

using System.Runtime.CompilerServices;

namespace MyTask;
class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("mian方法在异步方法 标志之前的线程:{0}", Environment.CurrentManagedThreadId);
        Task<int> baconTask = GetUrlContentLengthAsync(3);
        //   baconTask.ContinueWith(t => Console.WriteLine(t.Result));
 
        baconTask.Wait();
        Console.WriteLine("mian方法在异步方法 标志之后的线程:{0}", Environment.CurrentManagedThreadId);

        Console.Read();
    }
    static async Task<int> GetUrlContentLengthAsync(int slices)
    {
        Console.WriteLine("异步方法在await 标志之前的线程:{0}", Environment.CurrentManagedThreadId);

        HttpClient httpClient = new();
 
        // string content = await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");

        Task<string> getContent = httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");
        string content = await getContent;

        //await getContent反编译后是awaiter = 5__2.GetAwaiter();
        Console.WriteLine("异步方法在await 标志之后的线程:{0}",Environment.CurrentManagedThreadId);
      
        return content.Length;
    }
    static void DoInDependentWork()
    {

        Console.WriteLine($"content.Length:Working");

    }
}
/*
输出
mian方法在异步方法 标志之前的线程:1
异步方法在await 标志之前的线程:1
异步方法在await 标志之后的线程:10
mian方法在异步方法 标志之后的线程:1
*/

IL反编译后的代码

// MyTask.Program
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using MyTask;

[System.Runtime.CompilerServices.NullableContext(1)]
[System.Runtime.CompilerServices.Nullable(0)]
internal class Program
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        [System.Runtime.CompilerServices.Nullable(0)]
        public static readonly <>c <>9 = new <>c();

        [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })]
        public static Actionint>> <>9__0_0;

        internal void 
b__0_0(Task<int> t) { Console.WriteLine(t.Result); } } [CompilerGenerated] private sealed class d__1 : IAsyncStateMachine { public int <>1__state; [System.Runtime.CompilerServices.Nullable(0)] public AsyncTaskMethodBuilder<int> <>t__builder; public int slices; [System.Runtime.CompilerServices.Nullable(0)] private HttpClient 5__1; [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })] private Task<string> 5__2; [System.Runtime.CompilerServices.Nullable(0)] private string 5__3; [System.Runtime.CompilerServices.Nullable(0)] private string <>s__4; [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })] private TaskAwaiter<string> <>u__1; private void MoveNext() { int num = <>1__state; int length; try { TaskAwaiter<string> awaiter; if (num != 0) { 5__1 = new HttpClient(); 5__2 = 5__1.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); awaiter = 5__2.GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter; d__1 stateMachine = this; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } } else { awaiter = <>u__1; <>u__1 = default(TaskAwaiter<string>); num = (<>1__state = -1); } <>s__4 = awaiter.GetResult(); 5__3 = <>s__4; <>s__4 = null; DoInDependentWork(); length = 5__3.Length; } catch (Exception exception) { <>1__state = -2; 5__1 = null; 5__2 = null; 5__3 = null; <>t__builder.SetException(exception); return; } <>1__state = -2; 5__1 = null; 5__2 = null; 5__3 = null; <>t__builder.SetResult(length); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } } public static void Main(string[] args) { Task<int> baconTask = GetUrlContentLengthAsync(3); baconTask.ContinueWith(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Actionint>>(<>c.<>9.
b__0_0))); Console.Read(); } [AsyncStateMachine(typeof(d__1))] [DebuggerStepThrough] private static Task<int> GetUrlContentLengthAsync(int slices) { d__1 stateMachine = new d__1(); stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create(); stateMachine.slices = slices; stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start(ref stateMachine); return stateMachine.<>t__builder.Task; } private static void DoInDependentWork() { Console.WriteLine("content.Length:Working"); } }

 通过IL代码我们可以分析出整个代码运行过程 如下:

关系图中的数字对应于以下步骤,在调用方法调用异步方法时启动。

  1. Main调用方法调用并等待 GetUrlContentLengthAsync 异步方法。

  2. GetUrlContentLengthAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。

  3. GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 GetUrlContentLengthAsync

    GetStringAsync 返回 Task,其中 TResult 为字符串,并且 GetUrlContentLengthAsync 将任务分配给 getStringTask 变量。 该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。

  4. 由于尚未等待 getStringTask,因此,GetUrlContentLengthAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。

  5. DoIndependentWork 是完成其工作并返回其调用方的同步方法。

  6. GetUrlContentLengthAsync 已运行完毕,可以不受 getStringTask 的结果影响。 接下来,GetUrlContentLengthAsync 需要计算并返回已下载的字符串的长度,但该方法只有在获得字符串的情况下才能计算该值。

    因此,GetUrlContentLengthAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 GetUrlContentLengthAsync 的方法。 GetUrlContentLengthAsyncTask 返回给调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。

    备注

    如果 GetStringAsync(因此 getStringTask)在 GetUrlContentLengthAsync 等待前完成,则控制会保留在 GetUrlContentLengthAsync 中。 如果异步调用过程 getStringTask 已完成,并且 GetUrlContentLengthAsync 不必等待最终结果,则挂起然后返回到 GetUrlContentLengthAsync 将造成成本浪费。

    在调用方法中,处理模式会继续。 在等待结果前,调用方可以开展不依赖于 GetUrlContentLengthAsync 结果的其他工作,否则就需等待片刻。 调用方法等待 GetUrlContentLengthAsync,而 GetUrlContentLengthAsync 等待 GetStringAsync

  7. GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示 getStringTask 方法完成的任务中。 await 运算符从 getStringTask 中检索结果。   赋值语句将检索到的结果赋给 contents

  8. GetUrlContentLengthAsync 具有字符串结果时,该方法可以计算字符串长度。 然后,GetUrlContentLengthAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。 如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。

以上内容来自:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

 

 async\await的运行机理

反编译后我们可以看到async\await的运作机理主要分为分异步状态机和等待器,现在我主要来讲解着两部分的运行机制。

1、异步状态机

(1)异步状态机 始状态是-1;

(2)运行第一个【等待器】 期间异步状态机的状态是0。

(3)第一个【等待器】完成后异步状态机状态恢复-1。

(4)运行等二个【等待器】期间 异步状态机的状态是1,后面【等待器】以此类推。

(5)当所有的等待器都完成后,异步状态机的状态为-2。

异步状态机的组成

 

2、等待器

 TaskAwaiter等待器

第一个案例中代码await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");生成TaskAwaiter类型等待器。该类型等待器继承ITaskAwaiter接口

(1)有返回值的等待器,它返回值就是异步方法public Task GetStringAsync(string? requestUri)返回值Task 中的TResult。

此例子 中httpClient.GetStringAsync() 返回值是Task

所以等待器 TaskAwaiter的返回值是 string(Task中的string)。

 

ConfiguredTaskAwaiter等待器

 通过第 以下代码了解 等二种等待器类型

await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html").ConfigureAwait(false);生成ConfiguredTaskAwaitable.ConfiguredTaskAwaiter类型等待器.
该等待器将被设置成bool m_continueOnCapturedContext属性被设置成fasle,  该等待器继承IConfiguredTaskAwaiter接口。  

 StateMachineBoxAwareAwaiter等待器

 await Task.Yield();生成IStateMachineBoxAwareAwaiter类型等待器.,该类型的等待器继承自IStateMachineBoxAwareAwaiter接口

 Task.Yield 简单来说就是创建时就已经完成的 Task ,或者说执行时间为0的 Task ,或者说是空任务。
我们知道在 await 时会释放当前线程,等所 await 的 Task 完成时会从线程池中申请新的线程继续执行 await 之后的代码,这本来是为了解决异步操作(比如IO操作)霸占线程实际却用不到线程的问题,而 Task.Yield 却产生了一个不仅没有异步操作而且什么也不干的 Task ,不是吃饱了撑着吗?
真正的图谋是借助 await 实现线程的切换,让 await 之后的操作重新排队从线程池中申请线程继续执行。
将不太重要的比较耗时的操作放在新的线程(重新排队从线程池中申请到的线程)中执行。

3、通过IL代码,查看异步的执行过程,以及最后是如何把任务交给线程池运行

 异步代码大概运行顺序:

步骤1、主程序main调用异步f方法GetUrlContentLengthAsync()

步骤2、GetUrlContentLengthAsync();完成状态机初始化,然后调用状态机中异步任务方法构建器 中的开始函数:stateMachine.<>t__builder.Start(ref stateMachine);

步骤3、运行Start()函数中的AsyncMethodBuilderCore.Start(ref stateMachine);语句,保存当前线程的ExecutionContext(执行状态机)和SynchronizationContext(同步状态机),然后执行stateMachine.MoveNext();

该语句调用状态机的 MoveNext()方法;步骤4 开始进入状态机的MoveNext()方法;

步骤4、生成等待器 awaiter = 5__2.GetAwaiter(); 将状态机更改成0;然后运行<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);

 步骤5开始, 就是任务是一步一步被送入线程池运行,然后程序把控制权还给调用程序main。

步骤5、运行AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task);

步骤6、生成状态机盒子,将状态机和当前线程执行上下文 封装入状态机 代码如下:

IAsyncStateMachineBox stateMachineBox = GetStateMachineBox(ref stateMachine, ref taskField);

然后运行AwaitUnsafeOnCompleted(ref awaiter, stateMachineBox);

步骤7、执行以下代码。根据不同的等待器类型,选择进入askAwaiter.UnsafeOnCompletedInternal()方法中。

 //第一种等待器 获取同步上下文
     if (default(TAwaiter) != null && awaiter is ITaskAwaiter)
    {
        TaskAwaiter.UnsafeOnCompletedInternal(Unsafe.As(ref awaiter).m_task, box, true);
        return;
    }
    //第二种等待器   根据初始化设置决定要不要获取同步上下文
    if (default(TAwaiter) != null && awaiter is IConfiguredTaskAwaiter)
    {
        ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter reference = ref Unsafe.As(ref awaiter);
        TaskAwaiter.UnsafeOnCompletedInternal(reference.m_task, box, reference.m_continueOnCapturedContext);
        return;
    }
//第三种等待器
    if (default(TAwaiter) != null && awaiter is IStateMachineBoxAwareAwaiter)
    {
        try
        {
            ((IStateMachineBoxAwareAwaiter)(object)awaiter).AwaitUnsafeOnCompleted(box);
            return;
        }
        catch (Exception exception)
        {
            System.Threading.Tasks.Task.ThrowAsync(exception, null);
            return;
        }
    }
TaskAwaiter.UnsafeOnCompletedInternal方法声明如下:
internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)

 

步骤8、然后运行以下代码,进入 task.SetContinuationForAwait()方法中

internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
{
    if (TplEventSource.Log.IsEnabled() || Task.s_asyncDebuggingEnabled)
    {
        task.SetContinuationForAwait(OutputWaitEtwEvents(task, stateMachineBox.MoveNextAction), continueOnCapturedContext, false);
    }
    else
    {
        task.UnsafeSetContinuationForAwait(stateMachineBox, continueOnCapturedContext);
    }
}

运行OutputWaitEtwEvents(task, stateMachineBox.MoveNextAction,将任务和状态机的moveNext()封装成新的 委托。然后task.SetContinuationForAwait()运行该委托。

捕获当前同步上下文,

步骤9、运行以下代码, 这时候就用到了步骤7 提到的参数continueOnCapturedContext。由于步骤7传入是true 所以该方法就获取了SynchronizationContext同步上下文。

internal void SetContinuationForAwait(Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext)
{
    TaskContinuation taskContinuation = null;
    if (continueOnCapturedContext)
    {
        SynchronizationContext current = SynchronizationContext.Current;//获取SynchronizationContext同步上下文,控制台程序为null,ui程序该项不为null
        if (current != null && current.GetType() != typeof(SynchronizationContext))
        {
           //SynchronizationContext不等于null线程执行这一步
            taskContinuation = new SynchronizationContextAwaitTaskContinuation(current, continuationAction, flowExecutionContext);//将同步上希望一起封装入了实例TaskContinuation中
        }
        else
        {
           //SynchronizationContext等于null线程执行这一步
            TaskScheduler internalCurrent = TaskScheduler.InternalCurrent;
            if (internalCurrent != null && internalCurrent != TaskScheduler.Default)
            {
                taskContinuation = new TaskSchedulerAwaitTaskContinuation(internalCurrent, continuationAction, flowExecutionContext);
            }
        }
    }
    if (taskContinuation == null && flowExecutionContext)
    {
        taskContinuation = new AwaitTaskContinuation(continuationAction, true);
    }
    if (taskContinuation != null)
    {
        if (!AddTaskContinuation(taskContinuation, false))
        {
            
           //当线程池中的线程完成任务后该线程 会用taskContinuation.Run()中的SynchronizationContext同步上下文运行异步方法中剩下代码。//由于控制台没有显示的SynchronizationContext(控制台有默认的SynchronizationContext),
// 默认的SynchronizationContext没有转移控执行权的功能,所以控制台会继续使用多线程执行剩下异步方法中的代码。默认的SynchronizationContext内部有计数器来保证剩余的异步方法只执行一次。
// 但是ui线程有SynchronizationContext同步上下文DispachuerSynchronizationContext,该同步上下文会让会执行权转移到回到主线程,让主线程执行剩下的异步方法中的代码,如果主线程中调用方法中使用task.wait()或者task.result ,
//那么就会形成死锁。
taskContinuation.Run(this, false); } } else if (!AddTaskContinuation(continuationAction, false)) { //continueOnCapturedContext=false 执行这一步 AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this); } }

步骤10、跟踪到这里,终于看到真面目了,UnsafeQueueUserWorkItemInternal()方法向线程池的操作请求队列添加一个工作项以及其他数据项目,然后方法就会立即返回( 回到最开始的调用位置)。我知道还可以继续跟着,但我觉得跟踪到这边已经够了。

internal static void UnsafeScheduleAction(Action action, Task task)
{
    AwaitTaskContinuation awaitTaskContinuation = new AwaitTaskContinuation(action, false);
    TplEventSource log = TplEventSource.Log;
    if (log.IsEnabled() && task != null)
    {
        awaitTaskContinuation.m_continuationId = Task.NewId();
        log.AwaitTaskContinuationScheduled((task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id, task.Id, awaitTaskContinuation.m_continuationId);
    }
    ThreadPool.UnsafeQueueUserWorkItemInternal(awaitTaskContinuation, true);
}

 回到最开始的调用位置<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);

然后执行return;将控制权还给主程序。

private void MoveNext()
    {
        int num = <>1__state;
        int length;
        try
        {
            TaskAwaiter<string> awaiter;
            if (num != 0)
            {
                5__1 = new HttpClient();
                5__2 = 5__1.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");
                awaiter = 5__2.GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    d__1 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }

最终线程池中的某个线程会处理该工作项。

 

 

参考: https://www.cnblogs.com/lsxqw2004/p/4922374.html

C