为什么默认情况下所有函数都不应该是异步的? [英] Why shouldn't all functions be async by default?

查看:28
本文介绍了为什么默认情况下所有函数都不应该是异步的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

.net 4.5 的 async-await 模式是范式改变.这几乎好得令人难以置信.

The async-await pattern of .net 4.5 is paradigm changing. It's almost too good to be true.

我一直在将一些 IO 密集型代码移植到 async-await,因为阻塞已成为过去.

I've been porting some IO-heavy code to async-await because blocking is a thing of the past.

很多人将 async-await 与僵尸感染进行比较,我发现它相当准确.异步代码就像其他异步代码一样(你需要一个异步函数来等待一个异步函数).因此,越来越多的函数变得异步,这在您的代码库中不断增长.

Quite a few people are comparing async-await to a zombie infestation and I found it to be rather accurate. Async code likes other async code (you need an async function in order to await on an async function). So more and more functions become async and this keeps growing in your codebase.

将函数更改为异步是有些重复且缺乏想象力的工作.在声明中加入一个 async 关键字,用 Task<> 包装返回值,你就大功告成了.令人不安的是,整个过程是如此简单,而且很快,文本替换脚本将为我自动执行大部分移植"工作.

Changing functions to async is somewhat repetitive and unimaginative work. Throw an async keyword in the declaration, wrap the return value by Task<> and you're pretty much done. It's rather unsettling how easy the whole process is, and pretty soon a text-replacing script will automate most of the "porting" for me.

现在的问题是..如果我的所有代码都在慢慢变成异步,为什么不让它默认全部异步?

And now the question.. If all my code is slowly turning async, why not just make it all async by default?

我认为最明显的原因是性能.Async-await 有其开销和不需要异步的代码,最好不要.但是,如果性能是唯一的问题,那么肯定可以通过一些巧妙的优化在不需要时自动消除开销.我已经阅读了 快速路径" 优化,在我看来,它应该可以解决大部分问题.

The obvious reason I assume is performance. Async-await has a its overhead and code that doesn't need to be async, preferably shouldn't. But if performance is the sole problem, surely some clever optimizations can remove the overhead automatically when it's not needed. I've read about the "fast path" optimization, and it seems to me that it alone should take care of most of it.

也许这可以与垃圾收集器带来的范式转变相媲美.在早期的 GC 时代,释放自己的内存肯定更有效率.但是大众仍然选择自动收集,以支持更安全、更简单的代码,而这些代码可能效率较低(甚至可以说不再如此).也许这里应该是这种情况?为什么不是所有的函数都应该是异步的?

Maybe this is comparable to the paradigm shift brought on by garbage collectors. In the early GC days, freeing your own memory was definitely more efficient. But the masses still chose automatic collection in favor of safer, simpler code that might be less efficient (and even that arguably isn't true anymore). Maybe this should be the case here? Why shouldn't all functions be async?

推荐答案

首先,感谢您的美言.这确实是一个很棒的功能,我很高兴成为其中的一小部分.

First off, thank you for your kind words. It is indeed an awesome feature and I am glad to have been a small part of it.

如果我的所有代码都在慢慢变成异步,为什么不默认全部异步?

If all my code is slowly turning async, why not just make it all async by default?

好吧,你夸大其词了;所有您的代码没有变为异步.当您将两个普通"整数相加时,您无需等待结果.当你将两个未来整数相加得到第三个未来整数时——因为这就是Task,它是一个整数'将来会访问 - 当然你可能会等待结果.

Well, you're exaggerating; all your code isn't turning async. When you add two "plain" integers together, you're not awaiting the result. When you add two future integers together to get a third future integer -- because that's what Task<int> is, it's an integer that you're going to get access to in the future -- of course you'll likely be awaiting the result.

不使所有内容都异步的主要原因是因为async/await 的目的是使在具有许多高延迟操作的世界中更容易编写代码.您的绝大多数操作不是高延迟,因此采取降低延迟的性能损失没有任何意义.相反,您的关键少数操作具有高延迟,而这些操作正在导致整个代码中的异步僵尸感染.

The primary reason to not make everything async is because the purpose of async/await is to make it easier to write code in a world with many high latency operations. The vast majority of your operations are not high latency, so it doesn't make any sense to take the performance hit that mitigates that latency. Rather, a key few of your operations are high latency, and those operations are causing the zombie infestation of async throughout the code.

如果性能是唯一的问题,当然一些巧妙的优化可以在不需要时自动消除开销.

if performance is the sole problem, surely some clever optimizations can remove the overhead automatically when it's not needed.

理论上,理论和实践是相似的.实际上,它们从来都不是.

In theory, theory and practice are similar. In practice, they never are.

让我给你三点反对这种转换然后优化传递.

Let me give you three points against this sort of transformation followed by an optimization pass.

再次强调:C#/VB/F# 中的异步本质上是一种有限的继续传递形式.函数式语言社区进行了大量研究,以找出确定如何优化大量使用连续传递样式的代码的方法.在异步"是默认设置且非异步方法必须被识别和取消异步化的世界中,编译器团队可能不得不解决非常相似的问题.C# 团队对解决开放性研究问题并不真正感兴趣,因此这是反对的重点.

First point again is: async in C#/VB/F# is essentially a limited form of continuation passing. An enormous amount of research in the functional language community has gone into figuring out ways to identify how to optimize code that makes heavy use of continuation passing style. The compiler team would likely have to solve very similar problems in a world where "async" was the default and the non-async methods had to be identified and de-async-ified. The C# team is not really interested in taking on open research problems, so that's big points against right there.

反对的第二点是 C# 没有引用透明度"级别,这使得这些类型的优化更易于处理.我所说的参照透明度"是指表达式的值不依赖于它何时被求值的属性.像 2 + 2 这样的表达式是引用透明的;如果需要,您可以在编译时进行评估,或者将其推迟到运行时并获得相同的答案.但是像 x+y 这样的表达式不能及时移动,因为 x 和 y 可能会随时间变化.

A second point against is that C# does not have the level of "referential transparency" that makes these sorts of optimizations more tractable. By "referential transparency" I mean the property that the value of an expression does not depend on when it is evaluated. Expressions like 2 + 2 are referentially transparent; you can do the evaluation at compile time if you want, or defer it until runtime and get the same answer. But an expression like x+y can't be moved around in time because x and y might be changing over time.

异步使得推断副作用何时会发生变得更加困难.在异步之前,如果你说:

Async makes it much harder to reason about when a side effect will happen. Before async, if you said:

M();
N();

M()void M() { Q();R();},而 N()void N() { S();T();}RS产生副作用,那么你知道R的副作用发生在S的副作用之前.但是如果你有 async void M() { await Q();R();} 然后突然消失了.您无法保证 R() 是否会在 S() 之前或之后发生(当然除非等待 M() ;但是当然,它的 Task 不需要等到 N() 之后.)

and M() was void M() { Q(); R(); }, and N() was void N() { S(); T(); }, and R and S produce side effects, then you know that R's side effect happens before S's side effect. But if you have async void M() { await Q(); R(); } then suddenly that goes out the window. You have no guarantee whether R() is going to happen before or after S() (unless of course M() is awaited; but of course its Task need not be awaited until after N().)

现在想象一下,这个不再知道发生什么顺序副作用的特性适用于程序中的每一段代码,除了优化器设法消除的那些代码异步化.基本上你不知道哪个表达式会以什么顺序计算,这意味着所有表达式都需要引用透明,这在像 C# 这样的语言中很难.

Now imagine that this property of no longer knowing what order side effects happen in applies to every piece of code in your program except those that the optimizer manages to de-async-ify. Basically you have no clue anymore which expressions will be evaluate in what order, which means that all expressions need to be referentially transparent, which is hard in a language like C#.

反对的第三点是你必须问为什么异步如此特别?"如果您要争辩说每个操作实际上都应该是一个Task,那么您需要能够回答为什么不是Lazy?"或者为什么不Nullable?"或为什么不IEnumerable?"因为我们可以很容易地做到这一点.为什么不应该是每个操作都被提升为可空?或者每个操作都是惰性计算的,结果被缓存以备后用,或者每个操作的结果都是一个值序列而不是一个值.然后,您必须尝试优化那些您知道哦,这绝不能为空,以便我可以生成更好的代码"的情况,等等.(事实上​​,C# 编译器确实是为了提升算术.)

A third point against is that you then have to ask "why is async so special?" If you're going to argue that every operation should actually be a Task<T> then you need to be able to answer the question "why not Lazy<T>?" or "why not Nullable<T>?" or "why not IEnumerable<T>?" Because we could just as easily do that. Why shouldn't it be the case that every operation is lifted to nullable? Or every operation is lazily computed and the result is cached for later, or the result of every operation is a sequence of values instead of just a single value. You then have to try to optimize those situations where you know "oh, this must never be null, so I can generate better code", and so on. (And in fact the C# compiler does do so for lifted arithmetic.)

重点是:我不清楚Task<T> 是否真的有那么特别,需要做这么多工作.

Point being: it's not clear to me that Task<T> is actually that special to warrant this much work.

如果你对这些事情感兴趣,那么我建议你研究像 Haskell 这样的函数式语言,它具有更强的引用透明度,并允许各种无序评估和自动缓存.Haskell 在其类型系统中也为我提到的那种一元提升"提供了更强大的支持.

If these sorts of things interest you then I recommend you investigate functional languages like Haskell, that have much stronger referential transparency and permit all kinds of out-of-order evaluation and do automatic caching. Haskell also has much stronger support in its type system for the sorts of "monadic liftings" that I've alluded to.

这篇关于为什么默认情况下所有函数都不应该是异步的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆