c# - 为什么默认情况下所有函数都不应该是异步的?

标签 c# .net asynchronous async-await

async-await .net 4.5 的模式正在改变范式。这几乎好得令人难以置信。

我一直在将一些 IO 密集型代码移植到 async-await,因为阻塞已成为过去。

很多人将 async-await 与僵尸感染进行比较,我发现它相当准确。异步代码就像其他异步代码一样(你需要一个异步函数来等待一个异步函数)。因此,越来越多的函数变得异步,这在您的代码库中不断增长。

将函数更改为异步是有些重复且缺乏想象力的工作。扔一个 async声明中的关键字,用 Task<> 包裹返回值你已经完成了。令人不安的是整个过程是多么容易,很快一个文本替换脚本将为我自动化大部分“移植”。

现在的问题是..如果我的所有代码都在慢慢变成异步,为什么不默认让它全部异步呢?

我认为最明显的原因是性能。 Async-await 有其开销和不需要异步的代码,最好不要。但是,如果性能是唯一的问题,那么一些巧妙的优化肯定可以在不需要时自动消除开销。我读过 "fast path"优化,在我看来,它应该可以解决大部分问题。

也许这可以与垃圾收集器带来的范式转变相媲美。在早期的 GC 时代,释放自己的内存肯定更有效率。但是大众仍然选择自动收集,以支持更安全、更简单的代码,而这些代码可能效率较低(甚至可以说不再如此)。也许这里应该是这种情况?为什么不是所有的函数都应该是异步的?

最佳答案

首先,谢谢你的客气话。这确实是一个很棒的功能,我很高兴成为其中的一小部分。

If all my code is slowly turning async, why not just make it all async by default?



好吧,你夸张了; 全部 你的代码没有变成异步。当您将两个“普通”整数相加时,您无需等待结果。当你将两个 future 整数相加得到第三个 future 整数时——因为这就是 Task<int>也就是说,它是一个您将来要访问的整数——当然,您可能会等待结果。

不使所有内容异步的主要原因是因为 async/await 的目的是使在具有许多高延迟操作的世界中更容易编写代码 .您的绝大多数操作都不是高延迟,因此采取降低延迟的性能损失没有任何意义。相反,您的一些关键操作是高延迟,而这些操作正在导致整个代码中的异步僵尸感染。

if performance is the sole problem, surely some clever optimizations can remove the overhead automatically when it's not needed.



在理论上,理论和实践是相似的。在实践中,他们从来都不是。

让我给你三点反对这种转换,然后是优化传递。

第一点是:C#/VB/F# 中的异步本质上是一种有限的延续传递形式。函数式语言社区进行了大量研究,以找出确定如何优化大量使用连续传递样式的代码的方法。在默认情况下,“异步”是非异步方法并且必须识别和取消异步化的世界中,编译器团队可能必须解决非常相似的问题。 C# 团队对处理开放性研究问题并不真正感兴趣,所以这是反对的重点。

反对的第二点是 C# 没有“引用透明度”级别,这使得这些类型的优化更易于处理。我所说的“参照透明度”是指表达式的值不依赖于何时求值的属性。类似 2 + 2 的表达式参照透明;如果需要,您可以在编译时进行评估,或者将其推迟到运行时并获得相同的答案。但是像 x+y 这样的表达式无法及时移动,因为 x 和 y 可能会随时间变化。

异步使得推断副作用何时会发生变得更加困难。在异步之前,如果你说:
M();
N();

M()void M() { Q(); R(); } , 和 N()void N() { S(); T(); } , 和 RS产生副作用,那么你知道 R 的副作用发生在 S 的副作用之前。但如果你有 async void M() { await Q(); R(); }然后突然消失在窗外。您无法保证是否R()将在 S() 之前或之后发生(当然除非等待 M();但当然它的 Task 不需要等到 N() 之后。)

现在想象一下 的这个属性不再知道 中会发生什么顺序副作用适用于程序中的每一段代码除了优化器设法去异步化的那些。基本上你不知道哪个表达式将按什么顺序计算,这意味着所有表达式都需要引用透明,这在像 C# 这样的语言中很难。

反对的第三点是你必须问“为什么异步如此特别?”如果您要争辩说每个操作实际上都应该是 Task<T>那么你需要能够回答这个问题“为什么不Lazy<T>?”或“为什么不Nullable<T>?”或“为什么不IEnumerable<T>?”因为我们可以很容易地做到这一点。为什么不应该将每个操作都提升为可空?或者每个操作都被延迟计算,结果被缓存以备后用,或者每个操作的结果是一系列值而不是单个值。然后,您必须尝试优化那些您知道“哦,这绝不能为空,以便我可以生成更好的代码”的情况,等等。 (事实上​​,C# 编译器确实是为了提升算术而这样做的。)

重点是:我不清楚 Task<T>实际上是特别值得做这么多工作。

如果你对这些事情感兴趣,那么我建议你研究像 Haskell 这样的函数式语言,它具有更强的引用透明度,并允许各种无序评估和自动缓存。 Haskell 在其类型系统中也对我提到的那种“一元提升”提供了更强大的支持。

关于c# - 为什么默认情况下所有函数都不应该是异步的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18498942/

相关文章:

c# - MVVM-XAML中不同类的访问命令

c# - 关于 c# 中的 Cases 和 switches 我们真的需要它们吗?

c# - ConfigurationManager.AppSettings 越来越空?

c# - 你如何检查一个字符串是否存在于常量中

c# - 在 C# 5 中表示异步序列

node.js - Node.js 中的“异步”/'await' 在 Node.js v8.1.0 中不起作用

java - Play 框架 - 平衡异步和非阻塞

c# - 按销售额最多的厂商排名记录(数量*价格)

.net - 在构建服务器上运行 Roslyn 代码分析器

c# - Java 中 StringBuffer 的 .NET 等价物是什么?