C# 的 async/await 其实是stackless coroutine

2022-12-19 0 900

原副标题:C# 的 async/await 只不过是stackless coroutine

C# 的 async/await 只不过是两个通用型的触发器程式设计数学模型,C++会对 async 方式采用 CPS 转换,以 await 为边界线将方式进行分拆,接着采用两个自动机来驱动力执行。用现在比较潮的讲法是 stackless coroutine。

例如说以下标识符:

校对完之后就变为类似于这样的玩意儿(极其克雷姆斯兰县)

C# 的 async/await 其实是stackless coroutine

其中的 Scheduler 是交予采用者他们同时实现的,.NET 预设用缓存池来做 Scheduler,但并不阻碍许多架构能仿造两个该事件循环式来弄成 Java 的 Promise。.NET 还提供了 AsyncMethodBuilder 的 type trait 来让你他们同时实现那个自动机和你他们的 Task 类别,因此你能最大程度充分发挥想像来撰写你想控制的一切。

你能发现 async/await 这类并没有牵涉到任何缓存、运维相关的技术细节内容。换句话说,async/await 是两个纯C++优点。C# 在面世 async/await 的时候,考虑到方便快捷采用者的采用内建了 Task 和如前所述缓存池的运维器满足用户一般采用,我们通常叫那个为 async runtime,只不过是根据下面所言的 type trait 同时实现出来的东西,后来为的是减少重新分配又有了 ValueTask,以及 ASP .NET Core 为的是性能又他们同时实现了个 PooledValueTask,Blazor WebAssembly 又因为应用程序网络平台不支持多缓存只好同时实现了个如前所述该事件循环式的单缓存 Scheduler,Unity 街道社区还有他们做的 UniTask 之类。当然,采用者他们也能同时实现他们的 async runtime。而 C++、Rust 则一开始只是将其作为词汇优点面世,async runtime 则完全交予采用者同时实现,国际标准莫拉除了一些 async primitives 以外甚么都峭腹,只好 Rust 里出现了 async-std 和 tokio 等 async runtime,而 C++ 的话街道社区同时实现了一堆的 async runtime,接着国际标准理事会还瓦朗赛县圆笔怎么同时实现 STL 里的 async runtime。

啊,帕萨旺了,继续说优劣。

stackless coroutine 不须要暂存器语句存储和恢复,须要提及甚么局部变量只须要很单纯的将它们提高到那个自动机的闭袋中方可,而且是须要甚么才提高甚么,如果只有两个 int 须要提及,那就只须要两个分装后的 int 那么多二进制的堆缓存,约莫也就十多 B。此外,它遵从正常的分支判断和函数调用,因此不会打断 CPU 的控制流,分支预测和缓存友好。

而类似于 goroutine 的 stackful coroutine 方案则是在采用者态他们模拟系统缓存来做了个采用者态缓存,接着在运行时上他们运维,只好每两个 goroutine 都须要创建他们的 stack 用来保存语句(对应缓存的 stack),这两个 stack 是至少 8K,开多了占用会变得非常大。而且这种方案须要操作暂存器来进行语句的存储和恢复,会打断正常的 CPU 控制流,使得分支预测失误和缓存缺失问题非常严重。唯一的好处是不须要修改标识符,对已有老标识符改造起来非常方便快捷。但是如果不存在已有老标识符这种东西的话,这种方案能说一点优势都没有,我始终认为 stackful coroutine 只是两个在已有老标识符已经不方便快捷修改了才应该采用的东西,否则完全没有意义。

综上所述,async/await 是两个通用型的触发器程式设计方案,那个“触发器”和它的运维方式是不绑定的,由采用者想怎么同时实现就怎么同时实现,因此能做到最高的灵活度;并且由于不须要维护所谓的虚拟缓存的 stack,只须要将用到的返回值提高到闭包内方可,资源占用也很低;并且由于不打断 CPU 控制流,操控性更高,而且许多情况下是能直接把 coroutine inline 掉的。

缺点自然也很明显,那是标识符不好撰写,而且对标识符有侵入性。如果要用 async/await,那必然须要将所有须要改造的阻塞调用改成 async/await,否则就约等于没有触发器。

最后提一嘴操控性问题,网上大为流传的 Go vs C#, part 1: Goroutines vs Async-Await | by Alex Yakunin | Medium,其中给 C# 测试标识符加入了毫无意义的 await Task.YieldAsync,带来了大量无意义的运维开销,这等价于给测试标识符里面加 Thread.Sleep(1),何必呢。况且那个测试当时甚至用的是几乎一点优化都没有的 .NET Core 1.1,把原文用的 Go 和 .NET 升级到今天的 Go 1.19 和 .NET 7.0,C# 带着 await Task.YieldAsync 这一行故意负优化都能跑到跟 Go 不相上下,去掉之后更是只须要不到 Go 一半的时间。

而且由于前面所言的 stackful coroutine 缓存问题,没有大缓存是没法流畅跑完 Go 的测试的,因为 goroutine 的缓存开销非常大,在那个测试中须要耗费几个 G 的缓存,如果缓存不够会导致大量 GC 使得效率非常低下;对应 C# 的版本几乎没有甚么缓存消耗,十多 M 缓存就够跑完了。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务