为什么不应该默认所有的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(); }
, R
和S
产生副作用,那么你知道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.Sqrt
为Task<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版本,也是唯一的方法来保证你在未来不会遇到兼容性问题。