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

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

问题描述

.net 4.5的异步等待模式是范式改变。

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.

不少人将异步等待与僵尸侵扰进行了比较,我发现它相当准确。异步代码与其他异步代码一样(您需要一个异步函数才能等待一个异步函数)。因此,越来越多的功能变得异步,并且这在您的代码库中不断增长。

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< int> 的含义,这是您以后将要访问的整数-当然,您很可能在等待结果。

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.

不进行查询的主要原因一切都是异步的,因为异步/等待的目的是使在具有许多高延迟操作的世界中更容易编写代码。您的绝大多数操作都是 not 高延迟,因此减轻性能会降低延迟没有任何意义。相反,您的几个关键操作具有高延迟,并且这些操作导致整个代码中僵尸的异步感染。

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(); } R S 会产生副作用,那么您知道R的副作用发生在S的副作用之前。但是,如果您有 async void M(){等待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< T> ,那么您需要能够回答为什么不懒惰< T> ?或为什么 Nullable< T> 不?或为什么 IEnumerable< T> 不?因为我们可以轻松地做到这一点。为什么不将所有操作提升为可空的情况?或每个操作的计算都是延迟计算的,结果将被缓存以供以后使用,或者每个操作的结果都是一系列值而不是单个值。然后,您必须尝试优化那些您知道噢,它永远不能为null,这样我才能生成更好的代码的情况,依此类推。 (事实上​​,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天全站免登陆