为什么不应该默认所有的function是asynchronous的?

.net 4.5的asynchronous等待模式正在改变。 真是太好了。

我已经移植了一些IO密码,以便asynchronous等待,因为阻塞已成为过去。

不less人正在比较asynchronous的等待僵尸感染,我发现它是相当准确的。 asynchronous代码喜欢其他的asynchronous代码(你需要一个asynchronous函数来等待asynchronous函数)。 所以越来越多的函数变得asynchronous,并且在代码库中不断增长。

改变function到asynchronous是有点重复和难以想象的工作。 在声明中抛出一个async关键字,用Task<>包装返回值,你就完成了。 整个过程是多么容易让人不安,很快一个文本replace脚本会自动化大部分的“移植”。

现在的问题..如果我所有的代码慢慢地变成asynchronous,为什么不是默认的所有asynchronous呢?

我假设的显而易见的原因是性能。 asynchronous等待有一个开销和代码,不需要是asynchronous,最好不应该。 但是,如果性能是唯一的问题,那么当不需要的时候,一些聪明的优化可以自动消除开销。 我读过关于“快速path”的优化,在我看来,它本身应该照顾大部分。

也许这跟垃圾收集者所带来的范式转变是可比的。 在早期的GC时代,释放自己的记忆是绝对有效的。 但是大众仍然select自动收集,而不是更安全,更简单的代码,可能效率更低(甚至可以说是不成立的)。 也许这应该是这样的情况? 为什么不应该所有的function都是asynchronous的?

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

如果我所有的代码都慢慢变成asynchronous,为什么不把它全部设置为asynchronous呢?

好吧,你夸大了; 你所有的代码都不会变成asynchronous。 当你把两个“普通”整数加在一起时,你并没有等待结果。 当你将两个未来的整数加在一起得到第三个将来的整数时 – 因为这就是Task<int>是的,这是一个你将来可以访问的整数 – 当然你可能会等待结果。

不做所有asynchronous操作的主要原因是因为asynchronous/等待的目的是为了更容易在具有许多高延迟操作的世界中编写代码 。 绝大多数操作都不是高延迟,因此采用减less延迟的性能降低没有任何意义。 相反,您的几个关键操作是高延迟,这些操作导致整个代码中的asynchronous僵尸感染。

如果性能是唯一的问题,那么一些聪明的优化当然可以在不需要的时候自动移除开销。

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

让我给你三点反对这种转变,然后是优化通行证。

第一点是:C#/ VB / F#中的asynchronous本质上是延续传球的有限forms。 在函数式语言社区中进行了大量的研究,逐渐找出如何优化代码的方法,从而大量使用延续传递式。 编译器团队可能必须解决非常类似的问题,在这个世界上,“asynchronous”是默认的,非asynchronous方法必须被识别和asynchronous化。 C#团队对开放式研究问题并不感兴趣,所以这就是大问题。

第二点是,C#没有“引用透明度”的级别,这使得这些优化更容易处理。 “参照透明度”是指expression式的值在评估时不依赖于的属性。 像2 + 2这样的expression是透明的; 您可以在编译时进行评估,或者将其延迟到运行时间并获得相同的答案。 但是像x+y这样的expression式不能及时移动,因为x和y可能会随时间而改变

Async使得更难推理何时会发生副作用。 在asynchronous之前,如果你说:

 M(); N(); 

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

现在想象一下,除了优化器设法去除asynchronousify外, 不再知道顺序副作用发生的属性适用于程序中的每一段代码 。 基本上你不知道哪个expression式会按照什么顺序来评估,这就意味着所有的expression式都必须是引用透明的,这在C#这样的语言中很难。

第三点反对意见是你必须问“为什么asynchronous这么特别?” 如果你要争辩说,每一个操作都应该是一个Task<T>那么你需要能够回答“为什么不是Lazy<T> ?”这个问题。 或“为什么不可Nullable<T> ?” 或“为什么不IEnumerable<T> ?” 因为我们可以轻松地做到这一点。 为什么不应该把每一个操作都取消为空 ? 或者每一个操作都是懒散地计算出来的,并且结果被caching起来 ,或者每个操作的结果都是一系列的值而不是一个单一的值 。 然后,您必须尝试优化那些您知道“哦,这绝不能为空,所以我可以生成更好的代码”的情况,等等。 (事实上​​,C#编译器对提升的算术确实如此。)

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

如果你对这些东西感兴趣,那么我build议你研究像Haskell这样的函数式语言,它具有更强的参考透明性,允许各种乱序评估和自动caching。 Haskell在其types系统中对于我所提到的那种“monadic提升”也有更强的支持。

为什么不应该所有的function都是asynchronous的?

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

向后兼容性以及与其他语言(包括互操作场景)的兼容性也将成为问题。

另一个是复杂性和意图的问题。 asynchronous操作增加了复杂性 – 在许多情况下,语言function隐藏了这一点,但是在很多情况下,使async方法确实增加了复杂度。 如果您没有同步上下文,则尤其如此,因为asynchronous方法可能很容易导致导致意外的线程问题。

另外,还有很多例程本身并不是asynchronous的。 这些作为同步操作更有意义。 强制Math.SqrtTask<double> Math.SqrtAsync将是荒谬的,例如,因为没有任何理由是asynchronous的。 而不是通过你的应用程序async推动,你最终将await 在任何地方传播。

这也会彻底地打破目前的范式,并导致与属性有关的问题(这些问题实际上只是方法对,他们会不同步吗?),并在整个框架和语言devise中产生其他影响。

如果你做了很多IO绑定工作,你会发现普遍使用async是一个很好的补充,许多你的例程将是async 。 但是,当你开始进行CPU绑定的工作时,一般来说,使async实际上并不好 – 它隐藏了一个事实,即你正在使用看起来是asynchronous的API下的CPU周期,但实际上并不一定是真正的asynchronous。

除了性能 – asynchronous可以有一个生产力成本。 在客户端(WinForms,WPF,Windows Phone)上,这对生产力是一个福音。 但是在服务器上,或者在其他非UI场景中,您要付出高生产力。 你当然不希望在那里默认asynchronous。 在需要扩展性优势时使用它。

在甜蜜的地方使用它。 在其他情况下,不要。

我相信有一个很好的理由,如果不需要,所有的方法都是asynchronous的 – 可扩展性。 select性的制作方法asynchronous只有在你的代码永远不会发展,你知道方法A()总是CPU绑定的(你保持同步),而方法B()总是I / O绑定的(你把它标记为asynchronous)时才起作用。

但是如果事情发生了变化 是的,A()正在进行计算,但在将来的某个时刻,您必须在那里添加日志logging,报告或用户定义的callback,而实现无法预测,或者algorithm已经被扩展,现在不仅包括CPU计算,还有一些I / O? 您需要将该方法转换为asynchronous,但这会中断API,并且所有的堆栈调用者都需要更新(甚至可能是来自不同供应商的不同应用程序)。 或者你需要添加asynchronous版本,同步版本,但这并没有太大的区别 – 使用同步版本会阻止,因此是难以接受的。

如果可以在不更改API的情况下使现有同步方法asynchronous,那将是非常好的。 但是在现实中我们并没有这样的select,我相信,即使目前不需要使用asynchronous版本,也是唯一的方法来保证你在未来不会遇到兼容性问题。