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

Net 4.5的 异步-等待模式正在改变范式,它好得令人难以置信。

我一直在将一些 IO 代码移植到异步等待中,因为阻塞已经是过去的事情了。

相当多的人将异步-等待比作僵尸入侵,我发现它相当准确。异步代码与其他异步代码类似(您需要一个异步函数来等待一个异步函数)。因此,越来越多的函数变得异步,并且这在代码库中不断增长。

将函数更改为异步有些重复和缺乏想象力的工作。在声明中抛出一个 async关键字,用 Task<>包装返回值,这样就差不多完成了。整个过程是如此的简单,这让人很不安,很快一个文本替换脚本就会自动为我实现大部分的“移植”。

现在问题来了。.如果我所有的代码都在慢慢地变成异步的,为什么不在默认情况下把它们都变成异步的呢?

我认为最明显的原因是性能。异步-等待有它的开销和代码,不需要是异步的,最好不应该。但是,如果性能是唯一的问题,那么一些聪明的优化肯定可以在不需要时自动消除开销。我已经阅读了关于 "fast path"优化的文章,在我看来,只有它才能解决大部分问题。

也许这与垃圾收集器带来的范式转变类似。在早期的 GC 时代,释放您自己的内存肯定更有效。但是大多数人仍然选择自动收集,而倾向于更安全、更简单的代码,因为它们的效率可能更低(甚至可以说这种情况已经不再正确)。也许应该这样?为什么所有的函数不应该是异步的?

20394 次浏览

Why shouldn't all functions be async?

正如你提到的,表现是原因之一。请注意,您链接到的“快速路径”选项在完成 Task 的情况下确实提高了性能,但是与单个方法调用相比,它仍然需要更多的指令和开销。因此,即使有了“快速路径”,每个异步方法调用也会增加很多复杂性和开销。

Backwards compatibility, as well as compatibility with other languages (including interop scenarios), would also become problematic.

另一个是复杂性和意图的问题。异步操作增加了复杂性——在许多情况下,语言特性隐藏了这一点,但是在许多情况下,创建方法 async肯定会增加其使用的复杂性。如果您没有同步上下文,这种情况尤其如此,因为异步方法很容易导致意外的线程问题。

此外,还有许多本质上不是异步的例程。这些作为同步操作更有意义。例如,强制 Math.SqrtTask<double> Math.SqrtAsync将是荒谬的,因为根本没有理由使其为异步的。而不是让 async推通过您的应用程序,您将以 await传播 无处不在结束。

这也将彻底打破当前的范例,并导致与属性(实际上只是方法对)有关的问题。.它们也会异步吗?),并且在整个框架和语言的设计中有其他的反响。

如果您正在做大量的 IO 绑定工作,您会发现普遍使用 async是一个很好的补充,您的许多例程将是 async。然而,当你开始做 CPU 绑定的工作时,一般来说,使用 async实际上是不好的——它隐藏了这样一个事实: 你使用的 CPU 周期在一个 API 下,出现了是异步的,但实际上并不一定是真正的异步的。

除了性能以外-异步可能会带来生产力成本。在客户端(WinForms,WPF,WindowsPhone) ,这是一个生产力的福音。但是在服务器上,或者在其他非 UI 场景中,您的 付钱生产力。您肯定不希望在这里默认使用异步。当您需要可伸缩性优势时使用它。

Use it when at the sweet spot. In other cases, don't.

首先,谢谢你的夸奖。这确实是一个令人敬畏的功能,我很高兴成为其中的一小部分。

如果我所有的代码都在慢慢地变成异步的,为什么不在默认情况下把它们都变成异步的呢?

Well, you're exaggerating; 所有 your code isn't turning async. When you add two "plain" integers together, you're not awaiting the result. When you add two 未来整数 together to get a third 未来整数 -- because that's what Task<int> is, it's an integer that you're going to get access to in the future -- of course you'll likely be awaiting the result.

不使所有内容异步的主要原因是因为 异步/等待的目的是使得在一个有许多高延迟操作的世界中编写代码变得更加容易。您的绝大多数操作都是 没有高延迟,因此采取降低延迟的性能影响是没有任何意义的。相反,操作的 关键部分具有很高的延迟,这些操作会在整个代码中造成异步的僵尸感染。

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 and y might be changing over time

Async makes it much harder to reason about when a side effect will happen. Before async, if you said:

M();
N();

M()void M() { Q(); R(); }N()void N() { S(); T(); }RS产生副作用,那么你就知道 R 的副作用发生在 S 的副作用之前。但是如果你有 async void M() { await Q(); R(); },那么突然之间,它就消失了。你不能保证 R()是在 S()之前还是之后发生(当然除非 M()被等待; 但是当然它的 void M() { Q(); R(); }0不需要等到 N()之后)

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

第三个反对意见是,您必须问“为什么异步如此特殊?”如果你认为每个操作实际上都应该是 Task<T>,那么你需要能够回答这个问题“为什么不是 Lazy<T>?”或者“为什么不是 Nullable<T>?”或者“为什么不是 IEnumerable<T>?”因为我们也可以这么做。为什么不应该是这种情况下,将每个操作提升为可为空?或者 每个操作都是延迟计算的,结果将缓存以备后用或者 每个操作的结果都是一个值序列,而不仅仅是一个值。然后,您必须尝试优化那些您知道“哦,这绝对不能为 null,这样我就可以生成更好的代码”的情况,等等。(事实上,C # 编译器对提升算法也是这样做的。)

重点是: 我不清楚 Task<T>是否真的如此特别,以至于需要这么多的工作。

如果你对这些东西感兴趣,那么我建议你研究像 Haskell 这样的函数式语言,它们有更强的参照透明度(计算机科学) ,允许各种无序的计算和自动缓存。Haskell 在类型系统中也有更强大的支持,支持我提到的那种“单子提升”。

我相信,如果不需要所有方法的话,有一个很好的理由让它们变得异步——扩展性。选择性的异步方法只有在你的代码没有进化的情况下才能工作,你知道方法 A ()总是 CPU 绑定的(你保持同步) ,方法 B ()总是 I/O 绑定的(你标记它异步)。

But what if things change? Yes, A() is doing calculations but at some point in the future you had to add logging there, or reporting, or user-defined callback with implementation which cannot predict, or the algorithm has been extended and now includes not just CPU computations but also some I/O? You'll need to convert the method to async but this would break API and all the callers up the stack would be needed to be updated as well (and they can even be different apps from different vendors). Or you'll need to add async version alongside withe the sync version but this does not make much difference - using sync version would block and thus is hardly acceptable.

如果能够在不更改 API 的情况下使现有的 sync 方法异步,那就太好了。但实际上我们没有这样的选择,我相信,使用异步版本,即使目前不需要它是唯一的方法,以保证您永远不会遇到兼容性问题的未来。