首先,谢谢你的客气话.这确实是一个很棒的功能,我很高兴成为它的一小部分.
如果我所有的代码都在慢慢变为异步,为什么不在默认情况下使其全部异步呢?
嗯,你夸大其词了;all你的代码不是异步的.当您将两个"纯"整数相加时,您并不是在等待结果.当您将两个future integers相加得到第三个future integer时--因为这就是Task<int>
,它是一个整数,您将在将来访问它--当然,您可能会等待结果.
不将所有内容设置为异步的主要原因是the purpose of async/await is to make it easier to write code in a world with many high latency operations.您的绝大多数操作都是not高延迟,因此采取降低延迟的性能损失是没有任何意义的.相反,您的key few%的操作都是高延迟的,而这些操作正在导致整个代码中异步的僵尸出没.
如果性能是唯一的问题,那么一些巧妙的优化当然可以在不需要时自动消除开销.
在理论上,理论和实践是相似的.实际上,它们从来都不是.
让我给你三点建议,反对这种先进行优化再进行转换的做法.
第一点是:C#/VB/F#中的异步本质上是continuation passing的有限形式.函数式语言社区中的大量研究已经找到了确定如何优化大量使用延续传递风格的代码的方法.在"异步"是默认值,非异步方法必须被识别和反异步化的情况下,编译器团队可能必须解决非常类似的问题.C#团队对解决开放性研究问题并不感兴趣,所以这是他们面临的主要问题.
反对的第二点是C#没有使这类优化更容易处理的"引用透明性"级别.我所说的"参照透明性"是指the value of an expression does not depend on when it is evaluated.像2 + 2
这样的表达式在引用上是透明的;如果需要,您可以在编译时进行求值,或者将其推迟到运行时再得到相同的结果.但是像x+y
这样的表达式不能在时间上移动,因为x and y might be changing over time.
Async使人们更难推断副作用何时会发生.在异步之前,如果您说:
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()
之后).
现在假设这个属性no longer knowing what order side effects happen in适用于every piece of code in your program,除了优化器设法go 异步的那些.基本上你已经不知道哪些表达式将按什么顺序计算,这意味着所有表达式都需要是引用透明的,这在C#这样的语言中很难做到.
反对的第三点是,然后必须问"为什么异步如此特殊?"如果你认为每一个操作都应该是Task<T>
,那么你需要能够回答"为什么不是Lazy<T>
?"或者"为什么不呢?"或者"为什么不呢?"因为我们可以很容易做到.为什么不应该是every operation is lifted to nullable呢?或者every operation is lazily computed and the result is cached for later,或者the result of every operation is a sequence of values instead of just a single value.然后,您必须try 优化那些您知道"哦,这永远不能为null,这样我才能生成更好的代码"的情况,等等.(事实上,C#编译器对提升算法也是如此.)
重点是:我不清楚Task<T>
真的有那么特别,能保证这么多的工作.
如果您对这类事情感兴趣,那么我建议您研究像Haskell这样的函数式语言,它们具有更强的引用透明性,并且允许所有类型的无序计算和自动缓存.Haskell在其类型系统中对我提到的那种"一元式提升"也有更强的支持.