.NET 现在正在把 async/await 从原来的编译器实现改成 runtime 直接支持新的 async 调用约定,带来了非常大的性能提升,不过代码的写法倒是没有改变,但底层改变很大。
![]() |
1
geelaw 4 天前
省流版技术总结:在 IL 层面引入异步的概念,于是编译器不用把代码切成很多块儿(这会导致 JIT 很难掌握不变式,于是难以优化),于是 JIT 和运行时可以看到更多信息,从而优化异步性能。
向楼主提问:这套 ABI 是否有“反哺” iterator block 的可能/打算? |
![]() |
2
dcdlove 3 天前
怀念 ,由于脱钩搞信创 国内 C# 几乎被屠杀殆尽
|
![]() |
5
june4 3 天前
国内 .net 被打死了我是喜闻乐见的
|
![]() |
7
dcdlove 3 天前
|
8
nebkad 3 天前
我点句难听点的话,Rust 的 async/await 写起来虽然也不见得比 C# 的好到哪去,
但是等待异步事件不需要堆分配真的吊打 C#。 C# 吃了这么多年的老本,难道就没有考虑过优化这一点吗? 显然是压力不够并且保守群体太大,改不动。 现在微软已经重新走入堕落螺旋,我不相信 Runtime async 会有一个好的结果。 原因在于我上面说的,C# 现在连最有创造力的游戏开发群体都抓不住, 指望一堆吃老本的会用主动用新 runtime ,实在是过于乐观了。 |
9
hez2010 OP @nebkad 不知道你在说什么,runtime async 等待异步事件还真就可以不需要堆分配,不然文中递归调用 FibAsync 的测试中性能也不可能比得上同步版本。
|
10
nebkad 2 天前
@hez2010 我知道要出的这个 Runtime Async 现在可以做到不需要堆分配。
我的意思是这个优化来得太迟,社区里尤其是游戏开发者,一大堆手动打这个 patch 的实现,例如 UniTask, GDTask ,分别就是为 Unity3D 和 Godot 环境用的非堆上分配的异步库。 Rust 的 async await 在标准化的时候就不依赖于堆分配(当然也有别的限制,但是编译器会处理) 比起来就会显得 CLR 不思进取 |
11
hez2010 OP @nebkad UniTask 和 GDTask 哪怕有 Runtime Async 也是有必要存在的,因为需要自定义调度器实现。
比如 PIE 停止后需要停止调度 continuation ,自带的 Task 显然做不到,因为 async/await 的调度行为需要在 Task 或者 Task-like 类型来实现。 |
12
nebkad 2 天前
@hez2010
Task ValueTask UniTask GDTask 等等各种 task 必须有一个自定义调度器实现, 你又提醒了我一个理由,为什么不应该相信 Runtime async 会有一个好的结果。 看看 Rust 怎么做的,Future 只和 async/await 语法有关心,而任务调度则是异步框架的工作。 这样是不是更合理一点呢? |
13
hez2010 OP @nebkad C# 的各种 Task 实现不一定非得实现调度器,这个完全是可选的;而调度器本身也可以通过外部上下文传入使得与 Task 解藕。也就是说两种模式是同时支持的。
然而通过外部上下文来控制调度器并不具备强制性,有一万种方法在代码中绕过异步框架的调度器,甚至你在中途调用了某些实现垃圾的第三方异步代码给你丢弃掉上下文也不是不可能。而在类型里实现调度的话则能保证只要你在用这个类型就能得到 100%可预测的行为。 |
14
nebkad 1 天前
@hez2010
感谢你的科普。 这让我更了解了 C# async 底层的不堪。 C# 是可以允许不同的 Task UniTask 以及未来会产生的更多的的 Future/Promise 混用。 然后,如果我没理解错的话,它们背后的调度机制对使用者来说完全是黑魔法。 这将会进一步割裂 C# async 相关的库的生态,限制 C# 代码共享。 |
15
hez2010 OP ![]() @nebkad 使用者为什么要知道它背后的调度机制如何?使用者只需要知道“只要我把这个函数的返回值用 UniTask 包起来它的 continuation 就一定遵守 unity 的调度行为”。
举个例子: 返回 UniTask 的函数调用一个返回 Task 的异步函数,其中返回 Task 的异步函数是用来做 HTTP 请求,而返回 UniTask 的函数的 continuation 是用来根据响应更新游戏内的对象。 那此时 HTTP 请求的内部行为(比如异步 json 序列化等待)为什么要被扔进 unity 的 event loop ?他们完全可以采用 runtime 的标准调度行为,而等 HTTP 请求结束之后回到 UniTask 这边后,处理结果的时候采用 unity 的调度行为。 这即可以确保你的 HTTP 请求这种跟 unity 无关的东西不会挤占 unity event loop 的调度队列,同时又确保了游戏内 UniTask 的 continuation 全都被正确调度从而不会出现跨 unity 生命周期的游戏对象更新等等。 当然,有些人希望我不用 UniTask 也可以把 continuation 全都调度到主线程上,比如在 WPF 或者 winforms 里,那此时简单通过框架层面设置的同步上下文就可以决定你的 Task 的 continuation 在哪里执行。当然你也可以通过 .ConfigureAwait(false) 来手动针对某一处 await 绕过该行为。 另外我前面解释的一个地方有误,这里纠正一下。 前面说的“有一万种方法在代码中绕过异步框架的调度器,甚至你在中途调用了某些实现垃圾的第三方异步代码给你丢弃掉上下文也不是不可能”并不准确,实际上绕过这种行为只是针对你需要绕过的那一处 await 调用的局部行为,需要显式通过 .ConfigureAwait(false) 指定,使得该 await 之后的 continuation 不使用同步上下文: async Task Foo() { await Bar().ConfigureAwait(false); A(); // A 的执行将不受同步上下文控制 } 然而该异步函数 Foo 返回后,等待 Foo 的人的 continuation 仍然是遵守同步上下文进行调度的,因此不会产生任何的混乱问题。 |
16
hez2010 OP 另外补充一点,Unity 在框架层面也确实设置了同步上下文,因此你直接使用 Task 也不会有问题。
UniTask 的出现更多只是为了解决 Task 的分配问题(这一点 UniTask 内部通过池化和循环利用 awaitable 对象解决,而 runtime async 不存在这种问题),以及提供强保证防止误用(毕竟 UniTask 不提供 ConfigureAwait(false) 这种临时在局部扔掉同步上下文的方法)。即使没有 UniTask 你在 Unity 中全使用 Task 也不会出现什么问题。 等到 runtime async 出来之后我是能预计到会有相当一大部分的人从自定义 Task 类型回归到内置的 Task 的。 |