首先,谢谢您的客气话。这确实是一个了不起的功能,我很高兴成为其中的一小部分。
如果我所有的代码都在缓慢地变为异步状态,为什么不将其默认设置为异步呢?
好吧,你太夸张了;您所有的代码都不会变得异步。当您将两个“普通”整数相加时,就不必等待结果了。当您将两个将来的整数加在一起以得到第三个将来的整数时(因为那样的话Task<int>
,这是将来您将要访问的整数)当然您可能会等待结果。
不使所有内容都异步的主要原因是因为async / await的目的是使在具有许多高延迟操作的世界中更容易编写代码。绝大多数的操作都是不高的延迟,所以它没有任何意义,采取业绩击中其减轻该延迟。相反,您的关键操作中有几个是高延迟,而这些操作导致整个代码中僵死的异步侵扰。
如果性能是唯一的问题,那么肯定有一些聪明的优化可以在不需要时自动消除开销。
在理论上,理论和实践是相似的。实际上,它们从来都不是。
让我针对这种转换以及优化遍历给出三点意见。
同样,第一点是:C#/ VB / F#中的异步本质上是延续传递的一种有限形式。在功能语言社区中,大量研究已经找到了确定如何优化代码的方法,这些代码大量使用了延续传递样式。在默认情况下,“异步”是默认设置,必须识别和取消异步方法,在这种情况下,编译器团队可能必须解决非常相似的问题。C#团队对解决开放式研究问题并不十分感兴趣,因此这很重要。
反对的第二点是,C#没有“参照透明”级别,这使得这类优化更加容易处理。“参照透明性”是指表达式的值不依赖于何时对其求值的属性。类似2 + 2
的表达式是参照透明的;您可以根据需要在编译时进行评估,也可以将其推迟到运行时并得到相同的答案。但是像这样的表达式x+y
不能及时移动,因为x和y可能随着时间改变。
异步使推理何时发生副作用变得更加困难。在异步之前,如果您说:
M();
N();
和M()
是void M() { Q(); R(); }
,N()
曾经void N() { S(); T(); }
和R
并S
产生副作用,那么您知道R的副作用先于S的副作用发生。但是,如果您async void M() { await Q(); R(); }
突然遇到了,那便是窗外。您无法保证R()
会在发生之前还是之后发生S()
(当然,除非M()
等待;但是当然Task
不必等到之后)N()
。
现在想象一下,这种不再知道发生什么顺序副作用的属性适用于程序中的每段代码,除了优化程序设法取消异步处理的那些代码。基本上,您再也不知道哪个表达式将按什么顺序求值了,这意味着所有表达式都必须是参照透明的,这在像C#这样的语言中很难做到。
第三点是,您必须问“为什么异步如此特别?” 如果您要争论每个操作实际上应该是a,Task<T>
那么您需要能够回答“为什么不Lazy<T>
?” 这个问题。或“为什么不Nullable<T>
呢?” 或“为什么不IEnumerable<T>
呢?” 因为我们可以轻松地做到这一点。为什么不应该将每个操作都提升为可空值呢?或者,每个操作都是延迟计算的,结果将被缓存以备后用,或者每个操作的结果都是一个值序列,而不仅仅是一个值。然后,您必须尝试优化那些您知道“噢,这绝不能为null,这样我才能生成更好的代码”的情况,依此类推。
要点是:我不清楚Task<T>
实际上需要进行这么多工作的特殊之处。
如果您对这些事情感兴趣,那么我建议您研究诸如Haskell之类的功能语言,它们具有更强的引用透明度,并允许进行各种乱序评估并进行自动缓存。Haskell在其类型系统中也为我提到的“单子提升”提供了更强大的支持。