ET中的异步
本篇博客不止讨论ETTask如何实现, 更想探讨C#底层是如何支持异步实现的. 如果读者像笔者一样, 通过Unity接触到的C#语言, 可能对协程和异步概念的理解上有偏差, 因为我们在Unity中使用的协程并不是操作系统层次下讨论的与线程、进程、协程中的协程概念, Unity的协程是Unity基于IEnumerator和Unity事件更新框架实现的伪协程、伪异步, Unity的协程限制非常多, 如果读者对Unity的协程、IEnumerator和yield return
语法糖有疑惑, 欢迎参考IEnumerator与IEnumerable辨析和关于协程这两篇博客, 希望能帮助你理解.
本篇博客首先会讨论C#中异步的实现思路, 然后会讨论ETTask的实现思路, UniTask和YooAsset中的异步也在本系列的讨论之中.
对Task的概述
另外, 如果读者对C#中的异步不是很了解, 推荐先看一下下面五篇翻译的文章:
Dissecting the async methods in C#
Extending the async methods in C#
The performance characteristics of the async methods in C#
One user scenario to rule them all
A Deep Dive into C#’s Cancellation
在理解了上面博客中的内容后, 请思考这句话: Task是Task, Async是Async. 有Task并不一定意味着异步操作, 有Async也并不意味着一定有异步操作. 也就是说, 并不是只有在异步的场景下我们才可以使用Task, Task依然可以在同步场景下使用, 而async
关键字也不能完全和异步绑定, 因为async
关键字的作用只是告诉编译器对这个方法做一些特殊的处理: 每一个被标记为async的方法, Compiler在背后都会在其内部生成一个状态机.
为什么ET框架要设计自己的异步返回类型? 和Task相比, ET自己设计的异步返回类型有哪些优势.
⚠️Task有冷热之分
冷任务(Cold Task)不会自动执行,必须显式调用 Start()、RunSynchronously() 或通过任务调度器触发。
热任务(Hot Task)无需手动启动,任务在被创建时已经处于 Running 状态。
1 | private void Start() |
上面代码运行会报错:1
InvalidOperationException: Start may not be called on a promise-style task.
因为s是一个热任务, 在返回该任务时已经隐式Start了, 不必调用Start接口. 或者就按照它的报错信息理解, 通过非直接调用Task构造方法拿到的Task实例都是promise-style
的, 这种Task都不能调用Start.
ET作者猫大说: ETTask说自己是单线程的, 不支持多线程, 不像Task要支持多线程 ETTask做了什么?
请读者们想一想, 自己在用Task的时候, 从来没有调用过TaskAwaiter
或者说AsyncTaskMethodBuilder
的SetResult接口吧? 这是因为有TaskScheduler的存在, 在背后有一套自己的调度机制.
一个返回类型为Task的方法, 返回的是一个热任务, 该任务在被创建出来的那一刻就已经要给到TaskScheduler
进行管理了
Task可能一层一层地嵌套上来, 在业务使用上, 开发者最底层一般Task.Run()
或者Task.Delay
这样的接口, 上层的这些Task, 该任务在被创建出来的那一刻就已经要给到TaskScheduler
进行管理了, Task如何调度完全不受我们开发者的控制, 我们来回想一下我们是在哪一步将控制权转交给TaskScheduler的.
由于Task由TaskScheduler调度, 我们无法控制, 有可能涉及到多线程、出现上下文跨线程传递的开销, 因此ETTask的目标是自主控制调度、单线程作业, 你可以这么理解Task是C#的TaskScheduler, 来调用SetResult, 既然ETTask决定使用自己的异步机制, 那么就需要自己实现一个像TaskScheduler一样的调度机制, 在ProcessInnerSender
组件中, 就有一套ETTask的调度机制, 有一个requestCallback
ET框架中一共实现了三种用于异步操作的返回类型:ETVoid
、ETTask
和ETTask<T>
💡ETTask既是Awaiter又是可以被await的TaskLike类型, 希望不要对各位刚接触异步或ET的读者造成困扰
为什么ETTask里面有一个Coroutine
方法, 它的作用是什么?
SynchronizationContext
SynchronizationContext和ExecutionContext有什么联系吗?
SynchronizationContext中存储了一些能够标识线程身份的信息,现在你有一个方法,你可以通过SynchronizationContext.Send()
或者SynchronizationContext.Post
方法把你要执行的这个方法丢给你想要让他执行的线程里面去,可以把他理解为是一种跨线程的方法调用的方式。
在一般单线程里,方法的调用都是直来直去,而在多线程里面,可以通过SynchronizationContext来实现线程间的函数调用。
要注意一下Send和Post的区别,如果使用Send的方式把一个方法丢给某一个上下文,如果这个方法恰好很耗时,那么就会卡住调用Send处之后代码的执行,而如果使用Post方法的话,则不会阻塞调用处之后代码的执行。根据需求选择用Send还是Post。示例如下👇👇
1 | using System.Threading; |
TaskCompletionSource是什么?
按照笔者的理解,TaskCompletionSource可以将一个基于回调的异步操作转换成一个可以被await的异步操作。
ETTask与UniTask对比
扩展
参考文档
- C#中的TaskCompletionSource
- Dissecting the async methods in C#
- Extending the async methods in C#
- The performance characteristics of async methods in C#the-performance-characteristics-of-async-methods/
- One user scenario to rule them all
- async 的三大返回类型tdsourcetag=s_pcqq_aiomsg
- C# SynchronizationContext和ExecutionContext使用总结
- 详解 SynchronizationContext
- SynchronizationContext
- Parallel Computing - It’s All About the SynchronizationContextmsdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext
- 概述 .NET 6 ThreadPool 实现
- .NET Task 揭秘(1):什么是Task