经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
.NET 6.0 中的 await 原理浅析
来源:cnblogs  作者:Broadm  时间:2023/11/17 9:19:38  对本文有异议

前言

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

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

示例

  1. public class Program
  2. {
  3. static int Work()
  4. {
  5. Console.WriteLine("In Task.Run");
  6. return 1;
  7. }
  8. static async Task TestAsync()
  9. {
  10. Console.WriteLine("Before Task.Run");
  11. await Task.Run(Work);
  12. Console.WriteLine("After Task.Run");
  13. }
  14. static void Main()
  15. {
  16. _ = TestAsync();
  17. Console.WriteLine("End");
  18. Console.ReadKey();
  19. }
  20. }
  • 很简单的异步代码,我们来看下,编译器把它变成了啥
  • 编译后的代码经过我的整理,命名简化了,更容易理解
点击查看代码
  1. class Program
  2. {
  3. static int Work()
  4. {
  5. Console.WriteLine("In Task.Run");
  6. return 1;
  7. }
  8. static Task TestAsync()
  9. {
  10. var stateMachine = new StateMachine()
  11. {
  12. _builder = AsyncTaskMethodBuilder.Create(),
  13. _state = -1
  14. };
  15. stateMachine._builder.Start(ref stateMachine);
  16. return stateMachine._builder.Task;
  17. }
  18. static void Main()
  19. {
  20. _ = TestAsync();
  21. Console.WriteLine("End");
  22. Console.ReadKey();
  23. }
  24. class StateMachine : IAsyncStateMachine
  25. {
  26. public int _state;
  27. public AsyncTaskMethodBuilder _builder;
  28. private TaskAwaiter<int> _awaiter;
  29. void IAsyncStateMachine.MoveNext()
  30. {
  31. int num = _state;
  32. try
  33. {
  34. TaskAwaiter<int> awaiter;
  35. if (num != 0)
  36. {
  37. Console.WriteLine("Before Task.Run");
  38. awaiter = Task.Run(Work).GetAwaiter();
  39. if (!awaiter.IsCompleted)
  40. {
  41. _state = 0;
  42. _awaiter = awaiter;
  43. StateMachine stateMachine = this;
  44. _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
  45. return;
  46. }
  47. }
  48. else
  49. {
  50. awaiter = _awaiter;
  51. _awaiter = default;
  52. _state = -1;
  53. }
  54. awaiter.GetResult();
  55. Console.WriteLine("After Task.Run");
  56. }
  57. catch (Exception exception)
  58. {
  59. _state = -2;
  60. _builder.SetException(exception);
  61. return;
  62. }
  63. _state = -2;
  64. _builder.SetResult();
  65. }
  66. void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { }
  67. }
  68. }

状态机实现

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

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

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

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

  • 任务在线程池内被调度完成后,是怎么回到这个状态机继续执行后续代码的呢?
    _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 就是关键了, 跟下去,到了如下的代码:

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

线程池实现

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

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

    1. private void ExecuteWithThreadLocal(ref Task currentTaskSlot, Thread threadPoolThread = null)
    2. {
    3. this.InnerInvoke();
    4. this.Finish(true);
    5. }
    6. virtual void InnerInvoke()
    7. {
    8. Action action = this.m_action as Action;
    9. if (action != null)
    10. {
    11. action();
    12. return;
    13. }
    14. }
  • 很明显 this.InnerInvoke 就是执行了最开始 Task.Run(Work) 封装的委托了, 在 m_action 字段

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

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

    1. void RunContinuations(object continuationObject)
    2. {
    3. var asyncStateMachineBox = continuationObject as IAsyncStateMachineBox;
    4. if (asyncStateMachineBox != null)
    5. {
    6. AwaitTaskContinuation.RunOrScheduleAction(asyncStateMachineBox, flag2);
    7. return;
    8. }
    9. }
    10. static void RunOrScheduleAction(IAsyncStateMachineBox box, bool allowInlining)
    11. {
    12. if (allowInlining && AwaitTaskContinuation.IsValidLocationForInlining)
    13. {
    14. box.MoveNext();
    15. return;
    16. }
    17. }

总结

  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 的子类,哈哈,是不是明白了

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

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

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

原文链接:https://www.cnblogs.com/broadm/p/17833442.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号