125

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

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.

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?

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.

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?

talkol
  • 12,564
  • 11
  • 54
  • 64
  • Would you also argue that LINQ should be used wherever possible also? `Console.WriteLine` is a prime example of what should be synchronous to me – Sayse Aug 28 '13 at 22:13
  • 8
    Give the C# team the credit for marking the map. Like it was done hundreds of years ago, "dragons lie here". You could equip a ship and go there, pretty likely you'll survive it with the Sun shining and the wind in your back. And sometimes not, they never returned. Much like async/await, SO is filled with questions from users that didn't understand how they got off the map. Even though they got a pretty good warning. Now it's their problem, not the C# team's problem. They marked the dragons. – Hans Passant Aug 28 '13 at 22:18
  • 1
    @Sayse even if you remove the distinction between sync and async functions, calling functions that are implemented synchronously will still be synchronous (like your WriteLine example) – talkol Aug 28 '13 at 22:18
  • 2
    “wrap the return value by Task<>” Unless your method has to be `async` (to fulfill some contract), this is probably a bad idea. You're getting the disadvantages of async (increased cost of method calls; the necessity to use `await` in the calling code), but none of the advantages. – svick Aug 28 '13 at 22:50
  • 2
    This is a really interesting question but maybe not a great fit for SO. We might consider migrating it to Programmers. – Eric Lippert Aug 28 '13 at 23:02
  • @EricLippert I agree, discussions are a better fit there – talkol Aug 28 '13 at 23:04
  • Discussion continued here: http://programmers.stackexchange.com/questions/209872/blurring-the-lines-between-async-and-regular-functions-in-c-5-0 – talkol Aug 29 '13 at 21:19
  • 1
    Maybe I am missing something, but I have exactly the same question. If async/await always come in pairs and the code still have to wait for it to finish executing, when why not just update existing .NET framework, and make those methods that need to be async - async by default WITHOUT making up additional keywords? The language is already becoming to what it was designed to escape - keyword spaghetti. I think they should have stopped doing that after the "var" was suggested. Now we have "dynamic", asyn/await... etc... Why dont u guys just .NET-ify javascript? ;)) – monstro Dec 27 '14 at 18:22
  • This is interesting http://joeduffyblog.com/2015/11/19/asynchronous-everything/ – Tinku Nov 21 '15 at 06:02
  • This is an excellent foresight. This is not .NET specific. This is a general compute change. All functions and statements will be async by default (compiler will deal with / abstract). Only a matter of time. A vast majority of statements and code depend on IO, which is inherently async. It will be reversed and you will need to specify sync in the future (vs async). Now the only question is what new (and it will be new) language will be the first to notice and address this. It will be coined a new paradigm. Procedural -> object-oriented -> functional -> async-first/next. – pfeilbr Nov 23 '17 at 03:12
  • Also should note the CPU / architecture (von neumann) will not support an "async-first" programming model (too much overhead). This will need to be based on a new architecture (potentially quantum since its inherently async at the atomic level - time and computation decoupled, which is needed and very much possible for any "significant" advancement in computing in general). – pfeilbr Nov 23 '17 at 03:24
  • The universe is async. Sync only exists for the human brain to process information ( mere mortals :) ). The more computing is aligned with async / universe / reality , the faster computing (addressing real-world problems) will progress. – pfeilbr Nov 23 '17 at 03:30

4 Answers4

142

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?

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.

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.

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.

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();

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().)

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#.

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.)

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

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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 1
    Having async functions called without await doesn't make sense to me (in the common case). If we were to remove this feature, the compiler could decide by itself if a function is async or not (does it call await?). Then we could have identical syntax for both cases (async and sync), and only use await in calls as the differentiator. Zombie infestation solved :) – talkol Aug 28 '13 at 23:38
  • I've continued the discussion in programmers per your request: http://programmers.stackexchange.com/questions/209872/blurring-the-lines-between-async-and-regular-functions-in-c-5-0 – talkol Aug 29 '13 at 21:04
  • @EricLippert - Very nice answer as always :) I was curious if you could clarify "high latency"? Is there a general range in milliseconds here? I am just trying to figure out where the lower boundary line is for using async because I do not want to abuse it. – Travis J Nov 18 '13 at 18:15
  • 7
    @TravisJ: The guidance is: don't block the UI thread for more than 30 ms. Any more than that and you run the risk of the pause being noticeable by the user. – Eric Lippert Nov 18 '13 at 19:05
  • @EricLippert Every-time I read one of your answers to a question or your blog I feel like pulling out popcorn and ice-cream. You've answers are always wonderfully clear and informative. Thank you. – Maxim Gershkovich Oct 28 '17 at 15:44
  • I still don't understand - example HttpClient client = new HttpClient(); string urlContents = await client.GetStringAsync(); // Just remove Async postfix and instead of 2 methods you will have just 1 - client.GetString(); make it Async by default, why not? Any reason why client.GetString() is preferable to client.GetStringAsync(); ? – monstro Oct 01 '18 at 17:46
  • 5
    What challenges me is that whether or not something is done synchronously or asynchronously is an implementation detail that can change. But the change in that implementation forces a ripple effect through the code that calls it, the code that calls that, and so on. We end up changing code because of the code it depends on, which is something we normally go to great lengths to avoid. Or we use `async/await` because something hidden beneath layers of abstraction *might* be asynchronous. – Scott Hannen May 03 '19 at 14:50
  • It's worth noting that if you'd asked "well then why not make everything a Result<>" we notice that exceptions basically *were* the decision to lift everything into a result, and for high level programming I think it was probably the correct decision. – mako Dec 02 '22 at 02:54
23

Why shouldn't all functions be async?

Performance is one reason, as you mentioned. Note that the "fast path" option you linked to does improve performance in the case of a completed Task, but it still requires a lot more instructions and overhead compared to a single method call. As such, even with the "fast path" in place, you're adding a lot of complexity and overhead with each async method call.

Backwards compatibility, as well as compatibility with other languages (including interop scenarios), would also become problematic.

The other is a matter of complexity and intent. Asynchronous operations add complexity - in many cases, the language features hide this, but there are many cases where making methods async definitely adds complexity to their usage. This is especially true if you don't have a synchronization context, as the async methods then can easily end up causing threading issues that are unexpected.

In addition, there are many routines which aren't, by their nature, asynchronous. Those make more sense as synchronous operations. Forcing Math.Sqrt to be Task<double> Math.SqrtAsync would be ridiculous, for example, as there is no reason at all for that to be asynchronous. Instead of having async push through your application, you'd end up with await propogating everywhere.

This would also break the current paradigm completely, as well as cause issues with properties (which are effectively just method pairs.. would they go async too?), and have other repercussions throughout the design of the framework and language.

If you're doing a lot of IO bound work, you'll tend to find that using async pervasively is a great addition, an many of your routines will be async. However, when you start doing CPU bound work, in general, making things async is actually not good - it's hiding the fact that you're using CPU cycles under an API that appears to be asynchronous, but is really not necessarily truly asynchronous.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • Exactly what I was going to write (performance), backwards compatibility might be another thing, dll's to be used with older languages that don't support async/await also – Sayse Aug 28 '13 at 22:04
  • Making sqrt async isn't ridiculous if we simply remove the distinction between sync and async functions – talkol Aug 28 '13 at 22:05
  • @talkol I guess i'd turn this around - why *should* every function call take on the complexity of asynchrony? – Reed Copsey Aug 28 '13 at 22:09
  • Because the price you pay for accidentally making your code block is worst – talkol Aug 28 '13 at 22:10
  • 2
    @talkol I'd argue that's not necessarily true - asynchrony can add bugs itself that are worse than blocking... – Reed Copsey Aug 28 '13 at 22:11
  • Bugs in the infrastructure? or bugs in my code? Because infrastructure bugs will disappear just like they did with GC – talkol Aug 28 '13 at 22:12
  • @talkol No - async, in many cases, can introduce an entirely new realm of opportunity for bugs in your code. This is especially true when you start using async in scenarios without a synchronization context (ie: a console application)... All of a sudden, things start "jumping threads" and you get odd thread synchronization issues, etc. Async has a place, but that place isn't necessarily everywhere – Reed Copsey Aug 28 '13 at 22:14
  • Even if I decorate my synchronous code with async, it will still stay synchronous so arguably no "thread jumping" could happen. What's the harm in that? – talkol Aug 28 '13 at 22:31
  • @talkol Even if there was no harm, what's advantage in doing that? – svick Aug 28 '13 at 22:54
  • @svick advantage is a simpler and conistent syntax. Avoiding the domino effect when you need to make a single function async – talkol Aug 28 '13 at 22:57
  • 1
    @talkol How is `await FooAsync()` simpler than `Foo()`? And instead of small domino effect some of the time, you have huge domino effect all the time and you call that an improvement? – svick Aug 28 '13 at 23:52
5

Performance aside - async can have a productivity cost. On the client (WinForms, WPF, Windows Phone) it is a boon for productivity. But on the server, or in other non-UI scenarios, you pay productivity. You certainly don't want to go async by default there. Use it when you need the scalability advantages.

Use it when at the sweet spot. In other cases, don't.

usr
  • 168,620
  • 35
  • 240
  • 369
  • 2
    +1 - Simple attempt to have mental map of code running 5 asynchronous operations in parallel with random sequence of completion will for most people provide enough pain for a day. Reasoning about behavior of async (and hence inherently parallel) code is much harder than good old synchronous code... – Alexei Levenkov Aug 28 '13 at 22:31
5

I believe there is a good reason to make all methods async if they are not needed to - extensibility. Selective making methods async only works if your code never evolves and you know that method A() is always CPU-bound (you keep it sync) and method B() is always I/O bound (you mark it async).

But what if things change? Yes, A() is doing calculations but at some point in the future you had to add logging there, or reporting, or user-defined callback with implementation which cannot predict, or the algorithm has been extended and now includes not just CPU computations but also some I/O? You'll need to convert the method to async but this would break API and all the callers up the stack would be needed to be updated as well (and they can even be different apps from different vendors). Or you'll need to add async version alongside withe the sync version but this does not make much difference - using sync version would block and thus is hardly acceptable.

It would be great if it was possible to make the existing sync method async without changing the API. But in the reality we don't have such option, I believe, and using async version even if it's not currently needed is the only way to guarantee you'd never hit compatilibty issues in the future.

Alex
  • 2,469
  • 3
  • 28
  • 61
  • Though this seems like an extreme comment, there is a lot of truth in it. First, due to this issue: "this would break API and all the callers up the stack" async/await makes an applicaiton more tightly coupled. It could easily break the Liskov Substitution principle if a subclass wants to use async/await, for example. Also, it's difficult to imagine a microservices architecture where most methods didn't need async/await. – ItsAllABadJoke Feb 16 '20 at 23:49