13

Lately I can't seem to get enough of the amazing async-await pattern of C# 5.0. Where have you been all my life?

I'm absolutely thrilled with the simple syntax, but I'm having one small difficulty. My problem is that async functions have a totally different declaration from regular functions. Since only async functions can await on other async functions, when I'm trying to port some old blocking code to async, I'm having a domino effect of functions I have to convert.

People have been referring to this as a zombie infestation. When async gets a bite in your code, it will keep getting bigger and bigger. The porting process is not difficult, it's just throwing async in the declaration and wrapping the return value with Task<>. But it is annoying to do this over and over again when porting old synchronous code.

It seems to me it will be much more natural if both function types (async and plain old sync) had the exact same syntax. If this were the case, porting will take zero effort and I could switch painlessly between the two forms.

I think this could work if we follow these rules:

  1. Async functions won't require the async declaration anymore. Their return types wouldn't have to be wrapped in Task<>. The compiler will identify an async function during compilation by itself and do the Task<> wrapping automatically as needed.

  2. No more fire-and-forget calls to async functions. If you want to call an async function, you will need to await on it. I hardly use fire-and-forget anyways, and all the examples of crazy race conditions or deadlocks always seem to be based on them. I think they are too confusing and "out of touch" with the synchronous mindset we try to leverage.

  3. If you really can't live without fire-and-forget, there will be special syntax for it. In any case, it won't be part of the simple unified syntax I'm talking about.

  4. The only keyword you need to denote an asynchronous call is await. If you have await, the call is asynchronous. If you don't, the call is plain old synchronous (remember, we don't have fire-and-forget anymore).

  5. The compiler will identify async functions automatically (since they don't have a special declaration anymore). Rule 4 makes this very simple to do - if a function has an await call inside, it is async.

Could this work? or am I missing something? This unified syntax is much more fluid and could solve the zombie infestation altogether.

Some Examples:

// assume this is an async function (has await calls inside)
int CalcRemoteBalanceAsync() { ... }

// assume this is a regular sync function (has no await calls inside)
int CalcRemoteBalance() { ... }

// now let's try all combinations and see what they do:

// this is the common synchronous case - it will block
int b1 = CalcRemoteBalance();

// this is the common asynchronous case - it will not block
int b2 = await CalcRemoteBalanceAsync();

// choosing to call an asynchronous function in a synchronous manner - it will block
// this syntax was used previously for async fire-and-forget, but now it's just synchronous
int b3 = CalcRemoteBalanceAsync();

// strange combination - it will block since it's calling a synchronous function
// it should probably show a compilation warning though
int b4 = await CalcRemoteBalance();

Note: this is a continuation of an interesting related discussion in SO

talkol
  • 255
  • 1
  • 8
  • 3
    You always await on your asynchronous operations? Please tell me you don't do this immediately after firing them... – Jimmy Hoffa Aug 29 '13 at 22:21
  • Blocking on asynchronous operations is a very bad idea. The common case is that you block the UI thread, your asynchronous operation tries to resume on the UI thread, and you get a deadlock. How would your proposal avoid that? – svick Aug 29 '13 at 22:41
  • 1
    Also, one of the great things about async is that you don't have to `await` immediately. You can do something like `var task = FooAsync(); Bar(); await task;`. How would I do this in your proposal? – svick Aug 29 '13 at 22:43
  • 3
    SO is having a *discussion?* Where's my BFG-3000... – Robert Harvey Aug 29 '13 at 22:57
  • @JimmyHoffa What's wrong with awiting immediately after firing them? Await does not block the running thread – talkol Aug 30 '13 at 00:35
  • @svick (1) Excellent comment! I didn't work out all of the implementation details but I believe there are possible workarounds for avoiding deadlocks like nested contexts or thread spawning, but that's a whole different question see http://stackoverflow.com/questions/9343594/how-to-call-asynchronous-method-from-synchronous-method-in-c Ultimately when porting your UI thread code to async you will probably not change functions to async versions and block on them, so it's more of a boundary condition anyways.. – talkol Aug 30 '13 at 01:10
  • @svick (2) I would argue that these obscenities should not be supported by the simplified unified syntax ;) it's evil just like fire-and-forget and if you *really* want to do that, you should have special syntax for this purpose. – talkol Aug 30 '13 at 01:14
  • 2
    @talkol You think that parallel programming is obscene? That's an interesting outlook, to say the least, when you're talking about `async`. I think that's one of the big advantages of `async`-`await`: that it allows you to easily compose asynchronous operations (and not just in the simplest "start A, wait for A, start B, wait for B" way). And there already is special syntax exactly for this purpose: it's called `await`. – svick Aug 30 '13 at 01:19
  • 1
    @svick haha, now we've gone there :) I don't think parallel prog is obscene, but I think doing it with async-await is. Async-await is syntactic sugar for keeping your synchronous state of mind without paying the price of blocking. If you're already thinking in parallel, I would urge you to use a different pattern – talkol Aug 30 '13 at 01:25
  • talkol, To avoid confusion with existing semantics of async/await, like @Jimmy Hoffa had, and to presumably preserve your intent, I would suggest using «async» as a symbol when calling the functions, since using «await» in normal semantics means that the process will block until the called function returns. Whereas I think your idea is rather to use a symbol to indicate when _not_ to block the process which is making the call (so all functions can be declared the same way). It makes sense to use the ‘async’ keyword then, since it would indicate calling the function in an asynchronous manner. – Magne Jul 23 '21 at 11:06
  • It's interesting, no one really seems to have a good answer to your question. I too now fail to see why can't async be inferred, if a function uses await, it could automatically be made async. – Didier A. Oct 19 '21 at 03:35

1 Answers1

9

Your question is already answered in the SO question you linked.

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.

When WinRT first came out, the designers were describing how they decided which operations were going to be async. They decided that anything that was going to take 50ms or more would be async, and the remainder of the methods would be ordinary, non-asynchronous methods.

How many of the methods had to be rewritten to make them asynchronous? About 10 percent of them. The other 90% were not affected at all.

Eric Lippert goes on to explain in fairly substantial technical detail why they opted not to take a one-size-fits-all approach. He basically says that async and await are a partial implementation of continuation-passing style, and that optimizing such a style to fit all cases is a hard problem.

scrwtp
  • 4,532
  • 1
  • 24
  • 29
Robert Harvey
  • 198,589
  • 55
  • 464
  • 673
  • 1
    Please note the substantial difference between the SO question and this one. The SO asks why not make everything async. Here we don't suggest that, we suggest making 10% async, just using the same syntax for it that's all. Using a closer syntax has the advantage that you can more easily *change* which 10% is async, without suffering domino effects from these changes – talkol Aug 30 '13 at 00:48
  • I'm a little unclear on why `async` would produce zombie infestations. Even if a method calls 10 other methods, don't you just have to `async` the top-level one? – Robert Harvey Aug 30 '13 at 02:52
  • 8
    Let's say 100% of my current code is sync. Now I have a single internal leaf-level function that queries the DB that I would like to change to async. Now to make it async I need its caller to be async, and its caller to be async, and so forth until the top level. Of course I'm talking about the case where the entire chain is awaited upon (to keep the synchronous design of the code or pass return vales) – talkol Aug 30 '13 at 03:07