前言

看过不少关于 await 的原理的文章,也知道背后是编译器给转成了状态机实现的,但是具体是怎么完成的,回调又是如何衔接的,一直都没有搞清楚,这次下定决心把源码自己跑了下,终于豁然开朗了

本文的演示代码基于 VS2022 + .NET 6

示例

public class Program
{
static int Work()
{
Console.WriteLine("In Task.Run");
return 1;
} static async Task TestAsync()
{
Console.WriteLine("Before Task.Run");
await Task.Run(Work);
Console.WriteLine("After Task.Run");
} static void Main()
{
_ = TestAsync();
Console.WriteLine("End");
Console.ReadKey();
}
}
  • 很简单的异步代码,我们来看下,编译器把它变成了啥
class Program
{
static int Work()
{
Console.WriteLine("In Task.Run");
return 1;
} static Task TestAsync()
{
var stateMachine = new StateMachine()
{
_builder = AsyncTaskMethodBuilder.Create(),
_state = -1
};
stateMachine._builder.Start(ref stateMachine);
return stateMachine._builder.Task;
} static void Main()
{
_ = TestAsync();
Console.WriteLine("End");
Console.ReadKey();
} class StateMachine : IAsyncStateMachine
{
public int _state;
public AsyncTaskMethodBuilder _builder;
private TaskAwaiter<int> _awaiter; void IAsyncStateMachine.MoveNext()
{
int num = _state;
try
{
TaskAwaiter<int> awaiter;
if (num != 0)
{
Console.WriteLine("Before Task.Run");
awaiter = Task.Run(Work).GetAwaiter();
if (!awaiter.IsCompleted)
{
_state = 0;
_awaiter = awaiter;
StateMachine stateMachine = this;
_builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = _awaiter;
_awaiter = default;
_state = -1;
}
awaiter.GetResult();
Console.WriteLine("After Task.Run");
}
catch (Exception exception)
{
_state = -2;
_builder.SetException(exception);
return;
}
_state = -2;
_builder.SetResult();
} void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { }
}
}
  • 编译后的代码经过我的整理,命名简化了,更容易理解

状态机实现

  • 我们看到实际是生成了一个隐藏的状态机类 StateMachine

  • 把状态机的初始状态 _state 设置 -1

  • stateMachine._builder.Start(ref stateMachine); 启动状态机,内部实际调用的就是状态机的 MoveNext 方法

  • Task.Run 创建一个任务, 把委托放在 Task.m_action 字段,丢到线程池,等待调度

  • 任务在线程池内被调度完成后,是怎么回到这个状态机继续执行后续代码的呢?

    _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 就是关键了, 跟下去,到了如下的代码:

    if (!this.AddTaskContinuation(stateMachineBox, false))
    {
    ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, true);
    }
    bool AddTaskContinuation(object tc, bool addBeforeOthers)
    {
    return !this.IsCompleted && ((this.m_continuationObject == null && Interlocked.CompareExchange(ref this.m_continuationObject, tc, null) == null) || this.AddTaskContinuationComplex(tc, addBeforeOthers));
    }
    • 这里很清楚的看到,尝试把状态机对象(实际是状态机的包装类),赋值到 Task.m_continuationObject, 如果操作失败,则把状态机对象丢进线程池等待调度,这里为什么这么实现,看一下线程池是怎么执行的就清楚了

线程池实现

  • .NET6 的线程池实现,实际是放到了 PortableThreadPool, 具体调试步骤我就不放了,直接说结果就是, 线程池线程从任务队列中拿到任务后都执行了 DispatchWorkItem 方法
static void DispatchWorkItem(object workItem, Thread currentThread)
{
Task task = workItem as Task;
if (task != null)
{
task.ExecuteFromThreadPool(currentThread);
return;
}
Unsafe.As<IThreadPoolWorkItem>(workItem).Execute();
}
virtual void ExecuteFromThreadPool(Thread threadPoolThread)
{
this.ExecuteEntryUnsafe(threadPoolThread);
}
  • 我们看到, 线程池队列中的任务都是 object 类型的, 这里进行了类型判断, 如果是 Task , 直接执行 task.ExecuteFromThreadPool, 更有意思的这个方法是个虚方法,后面说明

  • ExecuteFromThreadPool 继续追下去,我们来到了这里,代码做了简化

    private void ExecuteWithThreadLocal(ref Task currentTaskSlot, Thread threadPoolThread = null)
    {
    this.InnerInvoke();
    this.Finish(true);
    } virtual void InnerInvoke()
    {
    Action action = this.m_action as Action;
    if (action != null)
    {
    action();
    return;
    }
    }
  • 很明显 this.InnerInvoke 就是执行了最开始 Task.Run(Work) 封装的委托了, 在 m_action 字段

  • this.Finish(true); 跟下去会发现会调用 FinishStageTwo 设置任务的完成状态,异常等, 继续调用 FinishStageThree 就来了重点: FinishContinuations 这个方法就是衔接后续回调的核心

    internal void FinishContinuations()
    {
    object obj = Interlocked.Exchange(ref this.m_continuationObject, Task.s_taskCompletionSentinel);
    if (obj != null)
    {
    this.RunContinuations(obj);
    }
    }
  • 还记得状态机实现么, Task.m_continuationObject 字段实际存储的就是状态机的包装类,这里线程池线程也会判断这个字段有值的话,就直接使用它执行后续代码了

    void RunContinuations(object continuationObject)
    {
    var asyncStateMachineBox = continuationObject as IAsyncStateMachineBox;
    if (asyncStateMachineBox != null)
    {
    AwaitTaskContinuation.RunOrScheduleAction(asyncStateMachineBox, flag2);
    return;
    }
    } static void RunOrScheduleAction(IAsyncStateMachineBox box, bool allowInlining)
    {
    if (allowInlining && AwaitTaskContinuation.IsValidLocationForInlining)
    {
    box.MoveNext();
    return;
    }
    }

总结

  1. Task.Run 创建 Task, 把委托放在 m_action 字段, 把 Task 压入线程池队列,等待调度
  2. _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 尝试把状态机对象放在 Task.m_continuationObject 字段上,等待线程池线程调度完成任务后使用(用来执行后续),若操作失败,直接把状态机对象压入线程池队列,等待调度
  3. 线程池线程调度任务完成后,会判断 Task.m_continuationObject 有值,直接执行它的 MoveNext

备注

  1. 状态机实现中,尝试修改 Task.m_continuationObject,可能会失败,

    就会直接把状态机对象压入线程池, 但是线程池调度,不都是判断是不是 Task 类型么, 其实状态机的包装类是 Task 的子类,哈哈,是不是明白了

    class AsyncStateMachineBox<TStateMachine> : Task<TResult>, IAsyncStateMachineBox where TStateMachine : IAsyncStateMachine
    
    static void DispatchWorkItem(object workItem, Thread currentThread)
    {
    Task task = workItem as Task;
    if (task != null)
    {
    task.ExecuteFromThreadPool(currentThread);
    return;
    }
    Unsafe.As<IThreadPoolWorkItem>(workItem).Execute();
    }
  • 还有就是状态机包装类,重写了 Task.ExecuteFromThreadPool,所以线程池调用 task.ExecuteFromThreadPool 就是直接调用了状态机的 MoveNext 了, Soga ^_^
    override void ExecuteFromThreadPool(Thread threadPoolThread)
    {
    this.MoveNext(threadPoolThread);
    }
参考链接
  • 关于线程池和异步的更深刻的原理,大家可以参考下面的文章

概述 .NET 6 ThreadPool 实现: https://www.cnblogs.com/eventhorizon/p/15316955.html

.NET Task 揭秘(2):Task 的回调执行与 await: https://www.cnblogs.com/eventhorizon/p/15912383.html

.NET6中的await原理浅析的更多相关文章

  1. 关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析

    关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析 如下代码,当我们在使用 ReentrantLock 进行加锁和解锁时,底层到底是如何帮助我们进行控制的啦 ...

  2. Javascript自执行匿名函数(function() { })()的原理浅析

    匿名函数就是没有函数名的函数.这篇文章主要介绍了Javascript自执行匿名函数(function() { })()的原理浅析的相关资料,需要的朋友可以参考下 函数是JavaScript中最灵活的一 ...

  3. Mysql 中的MVCC原理,undo日志的依赖

    一. MVCC 原理了解   原文点击:MVCC原理浅析 读锁: 也叫共享锁.S锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的 ...

  4. JAVA 8 主要新特性 ----------------(二)版本中数据结构的修改浅析

    一.版本中数据结构的修改浅析1.HashMap.HashSet.ConcurrentHashMap的数据结构发生变化 (1)HashMap简介(结构:哈希表+链表) HashMap存储的数据是无序的, ...

  5. [转帖]Git数据存储的原理浅析

    Git数据存储的原理浅析 https://segmentfault.com/a/1190000016320008   写作背景 进来在闲暇的时间里在看一些关系P2P网络的拓扑发现的内容,重点关注了Ma ...

  6. Android-Binder原理浅析

    Android-Binder原理浅析 学习自 <Android开发艺术探索> 写在前头 在上一章,我们简单的了解了一下Binder并且通过 AIDL完成了一个IPC的DEMO.你可能会好奇 ...

  7. Dubbo学习(一) Dubbo原理浅析

    一.初入Dubbo Dubbo学习文档: http://dubbo.incubator.apache.org/books/dubbo-user-book/ http://dubbo.incubator ...

  8. 沉淀,再出发:docker的原理浅析

    沉淀,再出发:docker的原理浅析 一.前言 在我们使用docker的时候,很多情况下我们对于一些概念的理解是停留在名称和用法的地步,如果更进一步理解了docker的本质,我们的技术一定会有质的进步 ...

  9. 阻塞和唤醒线程——LockSupport功能简介及原理浅析

    目录 1.LockSupport功能简介 1.1 使用wait,notify阻塞唤醒线程 1.2 使用LockSupport阻塞唤醒线程 2. LockSupport的其他特色 2.1 可以先唤醒线程 ...

  10. 【Spark Core】TaskScheduler源代码与任务提交原理浅析2

    引言 上一节<TaskScheduler源代码与任务提交原理浅析1>介绍了TaskScheduler的创建过程,在这一节中,我将承接<Stage生成和Stage源代码浅析>中的 ...

随机推荐

  1. 使用wsl 清理windows 下的C盘

    大文件删除思路 ## 在wsl 中可以看到,C盘已经挂载了,挂载点为/mnt/c dewan@wsl ~% df /mnt/c Filesystem Size Used Avail Use% Moun ...

  2. std::queue 中遇到释放内存错误的问题

    项目上有个需求要用到 std::queue 顺序处理消息事件 简单的示例如下: struct MyEvent { MyEvent() { event_ = CreateEvent(nullptr, 0 ...

  3. 八 Appium常用方法介绍(转)

    由于appium是扩展了Webdriver协议,所以可以使用webdriver提供的方法,比如在处理webview页面,完全可以使用webdriver中的方法.当然在原生应用中,也可以使用. 1.元素 ...

  4. P1941 [NOIP2014 提高组] 飞扬的小鸟 题解

    我们先不管障碍物. 设 \(f[i][j]\) 表示来到点 \((i,j)\) 的最少点击屏幕数. 因为每秒要不上升 \(k\times x[i]\),要么下降 \(y[i]\). 所以有: \[f[ ...

  5. 超详细的mysql总结(DQL)

    上一篇文章总结了 DDL.DML的使用,这一篇文章把剩下的 DQL 加上~   DQL(Data Query Language)即数据库查询语言,用来查询所需要的信息,在查询的过程中,需要判断所查询的 ...

  6. Maven资源导出问题所需配置

    <!--在build中配置resources,来防止我们资源导出失败的问题--> <build> <resources> <resource> < ...

  7. 痞子衡嵌入式:AppCodeHub - 一站网罗恩智浦MCU应用程序

    近日,恩智浦官方隆重上线了应用程序代码中心(Application Code Hub,简称 ACH),这是恩智浦 MCUXpresso 软件生态的一个重要组成部分.痞子衡之所以要如此激动地告诉大家这个 ...

  8. 记一次Android项目升级Kotlin版本(1.5 -> 1.7)

    原文地址: 记一次Android项目升级Kotlin版本(1.5 -> 1.7) - Stars-One的杂货小窝 由于自己的历史项目Kotlin版本比较老了,之前已经升级过一次了(1.4-&g ...

  9. 极简工作流「GitHub 热点速览」

    原以为 LLM 很难,但其实可以很简单,比如 Flowise 拖拽拖拽就能出来一个 LLM 流程,非常简单你的 LLM 就可以 run 起来了.同样的 web-check 也能极快速地帮你解决 Web ...

  10. python 运行环境变为 pytest in (for) xxx.py原因

    因为本人的自定义函数名称开头为test,在.py文件内我用了unittest框架,所以环境随着变化了. 修改回去很简单,只要不使用test开头或者换个文件夹.