tal*_*kol 103 .net c# asynchronous async-await
.net 4.5 的async-await模式是范式的变化.这真是太好了.
我一直在将一些IO重的代码移植到async-await,因为阻塞已成为过去.
相当多的人正在比较async-await和僵尸感染,我发现它相当准确.异步代码喜欢其他异步代码(您需要异步函数才能等待异步函数).因此,越来越多的函数变得异步,并且在代码库中不断增长.
将函数更改为异步是一种重复性和缺乏想象力的工作.async在声明中抛出一个关键字,包装返回值Task<>,你就完成了.令人不安的是整个过程是多么容易,并且很快文本替换脚本将为我自动化大部分"移植".
现在的问题是..如果我的所有代码都慢慢变为异步,为什么不默认将它全部变为异步?
我假设的显而易见的原因是表现.Async-await有它的开销和代码,不需要异步,最好不要.但是,如果性能是唯一的问题,那么一些聪明的优化肯定可以在不需要时自动消除开销.我已经读过关于"快速路径"优化的内容,在我看来,它应该只关注它的大部分内容.
也许这与垃圾收集者带来的范式转变相当.在GC早期,释放自己的记忆肯定更有效率.但群众仍然选择自动收集,转而采用更安全,更简单的代码,这些代码可能效率较低(甚至可能不再适用).也许这应该是这样的情况?为什么不能将所有函数都异步?
Eri*_*ert 117
首先,谢谢你的客气话.这确实是一个很棒的功能,我很高兴成为它的一小部分.
如果我的所有代码都慢慢变为异步,为什么不默认将它全部变为异步?
好吧,你夸张了; 你的所有代码都没有异步.当您将两个"普通"整数添加到一起时,您不会等待结果.当你将两个未来的整数加在一起得到第三个未来的整数时 - 因为那是什么Task<int>,它是一个你将来可以访问的整数 - 当然你可能正在等待结果.
不使所有异步同步的主要原因是因为async/await的目的是使在具有许多高延迟操作的世界中编写代码更容易.绝大多数的操作都是不高的延迟,所以它没有任何意义,采取减轻该延迟对性能的影响.相反,您的一些关键操作是高延迟,而这些操作导致整个代码中的异步僵尸感染.
如果性能是唯一的问题,肯定一些聪明的优化可以在不需要时自动消除开销.
理论上,理论和实践是相似的.在实践中,他们永远不会.
让我给出三点反对这种转换,然后是优化传递.
第一点是:C#/ VB/F#中的异步本质上是一种有限形式的延续传递.函数式语言社区中的大量研究已经开始找出如何优化代码的方法,这些代码大量使用了延续传递方式.编译器团队可能必须解决非常类似的问题,在这个问题中,"async"是默认的,并且必须识别非异步方法并解除异步.C#团队并不真正对开展研究问题感兴趣,所以这是对抗那里的重点.
第二点反对是C#没有"参照透明度"的水平,这使得这些优化更容易处理."引用透明度"是指表达式的值不依赖于何时进行求值的属性.像这样2 + 2的表达是透明的; 如果需要,可以在编译时进行评估,或者将其推迟到运行时并获得相同的答案.但是x+y,由于x和y可能会随着时间的推移而变化,因此无法及时移动表达式.
Async使得更难以推断副作用何时发生.在异步之前,如果你说:
M();
N();
Run Code Online (Sandbox Code Playgroud)
并且M()是void M() { Q(); R(); }和N()是void N() { S(); T(); },与R和S产生副作用,那么你知道,S前的副作用的r的副作用发生.但是,如果你async void M() { await Q(); R(); }突然那么就会出现在窗外.您无法保证是否R()会在之前或之后发生S()(当然除非M()等待;但当然,Task直到之后才能等待它N().)
现在想象一下,这个不再知道顺序副作用发生的属性适用于程序中的每一段代码,除了那些优化器设法解除异步的代码.基本上你不再知道哪些表达式将以什么顺序进行评估,这意味着所有表达式都需要是引用透明的,这在C#这样的语言中很难实现.
第三点反对的是,你必须问"为什么异步如此特殊?" 如果你要争辩说每个操作实际上应该是一个,Task<T>那么你需要能够回答"为什么不Lazy<T>呢?" 这个问题.或"为什么不Nullable<T>呢?" 或"为什么不IEnumerable<T>呢?" 因为我们可以轻松地做到这一点.为什么不应该将每个操作都解除为可空?或者每个操作都是延迟计算的,结果会缓存以供日后使用,或者每个操作的结果都是一系列值而不是一个值.然后,您必须尝试优化那些您知道"哦,这绝不能为空,因此我可以生成更好的代码"的情况,等等.(事实上,C#编译器确实可以用于提升算术.)
重点是:我不清楚Task<T>实际上是否需要保证这么多工作.
如果您感兴趣这些类型的东西,那么我建议您研究像Haskell这样的函数式语言,它们具有更强的引用透明性并允许各种无序评估并进行自动缓存.Haskell在其类型系统中也有更强大的支持,用于我所提到的各种"monadic提升".
Ree*_*sey 21
为什么不能将所有函数都异步?
正如你所提到的,性能是一个原因.请注意,链接到的"快速路径"选项确实在完成任务的情况下提高了性能,但与单个方法调用相比,它仍然需要更多的指令和开销.因此,即使使用"快速路径",每次异步方法调用也会增加很多复杂性和开销.
向后兼容性以及与其他语言(包括互操作方案)的兼容性也会成为问题.
另一个是复杂性和意图问题.异步操作增加了复杂性 - 在许多情况下,语言功能隐藏了这一点,但在很多情况下,制作方法async肯定会增加其使用的复杂性.如果您没有同步上下文,则尤其如此,因为异步方法很容易导致导致意外的线程问题.
此外,有许多例程本质上不是异步的.那些作为同步操作更有意义.例如,强迫Math.Sqrt它Task<double> Math.SqrtAsync是荒谬的,因为没有任何理由可以异步.而不是async通过你的应用程序,你最终会到处await传播.
这也会彻底打破当前的范式,并导致属性问题(实际上只是方法对......它们也会异步吗?),并且在整个框架和语言设计中会产生其他影响.
如果你正在进行大量的IO绑定工作,你会发现async普遍使用是一个很好的补充,你的许多例程都将是async.但是,当您开始执行CPU绑定工作时,通常情况下,制作async实际上并不好 - 它隐藏了这样一个事实,即您在一个似乎是异步的API下使用CPU周期,但实际上并不一定是真正的异步.
抛开性能不谈 - 异步可能会产生生产力成本。在客户端(WinForms、WPF、Windows Phone)上,它是生产力的福音。但是在服务器上,或在其他非 UI 场景中,您需要支付生产力。你当然不想在那里默认异步。当您需要可扩展性优势时使用它。
在最佳位置时使用它。在其他情况下,不要。
我相信如果不需要的话,让所有方法异步是有充分理由的——可扩展性。仅当您的代码永远不会发展并且您知道方法 A() 始终受 CPU 限制(您保持同步)并且方法 B() 始终受 I/O 限制(您将其标记为异步)时,选择性地使方法异步才有效。
但如果事情发生变化怎么办?是的,A() 正在执行计算,但在将来的某个时刻,您必须在那里添加日志记录、报告或用户定义的回调以及无法预测的实现,或者算法已扩展,现在不仅包括 CPU 计算,还包括还有一些 I/O?您需要将该方法转换为异步,但这会破坏 API,并且堆栈上的所有调用者也需要更新(它们甚至可以是来自不同供应商的不同应用程序)。或者您需要将异步版本与同步版本一起添加,但这并没有太大区别 - 使用同步版本会阻塞,因此很难接受。
如果能够在不更改 API 的情况下使现有的同步方法异步,那就太好了。但实际上我们没有这样的选择,我相信,即使当前不需要,使用异步版本也是保证您将来永远不会遇到兼容性问题的唯一方法。