Hacker News new | ask | show | jobs
by SideburnsOfDoom 4847 days ago
Inside every language there is a minimal language with all the syntactic sugar translated into a subset of the language.

e.g. the C# compiler turns "var a = 3;" into "int a = 3;" It turns anonymous lambdas into methods with generated names (i.e turns one line of code into 3-4 lines), and makes classes with constructors that do the environment capture if needed. It turns "yield return" generator methods into objects that have state.

While async/await is a cool feature and is worth using, it is worth noting the up trend in complexity of generated code – and ansyc/await generates significantly more code in the simpler language without this feature than previous new features did.

How much code does an await statement give rise to? it looks like about 40-50 lines to me.

4 comments

You need to compare it to the alternatives. For instance how much IL will be procuded from creating Tasks with Continuations?

Async/Await makes it easier to write asynchronous code in a traditional linear manner, it's easy to shoot yourself in the foot but it's just as easy with the alternatives.

Here's an article that I wrote a while back on what Async & Await generates: http://blog.filipekberg.se/2013/01/16/what-does-async-await-...

Personally I think the Async/Await model is too limiting. E.g. lets talk about this:

    var result = await client.GetStringAsync("http://msdn.microsoft.com");
From the perspective of the caller, that code is still blocking. It solves the problem of threads being blocked, but that's only one of the problems you have.

The real and more complete alternative would be to work with a well designed Future/Promise framework, like the one in Scala [1] which is also usable from Java [2]. Doing concurrent computations by means of Futures/Promises is like working with Lego blocks.

Let me exemplify with a piece of code that's similar to what I'm using in production. Lets say that you want to make 2 requests to 2 different endpoints that serve similar results. You want the first one that completes to win (like an auction), but in case of a returned error, you want to fallback on the other one (type annotations and extra verbosity added for clarity):

    val url1 = "http://some.url.com"
    val url2 = "http://other.url.com"

    // initiating first concurrent request
    val fr1: Future[Response] = client.get(url1)

    // initiating second concurrent request
    val fr2: Future[Response] = client.get(url2)

    // the first fallbacks on the second in case of error
    val firstWithFallback: Future[Response] =  
      fr1.fallbackTo(fr2)

    // the second fallbacks on the first in case of error
    val secondWithFallback: Future[Response] = 
      fr2.fallbackTo(fr1)

    // pick a winner
    val finalResponse: Future[Response] = 
      Future.firstCompletedOf(firstWithFallback :: secondWithFallback :: Nil)

    // process the result
    val string: Future[String] = finalResponse.map(r => r.body)

    // in case both endpoints failed, log the error and 
    // fallback on a local copy
    val stringWithFallback: Future[String] = string.recover {
      case ex: Exception =>
        logger.error(ex)
        File.read("/something.txt")
    }
Given an HTTP client that's based on NIO, the above code is totally non-blocking. You can do many other crazy things. Like you can wait for several requests to complete and get a list of results in return. Or you can try the first url and if it fails, try the second and so on, until one of them succeeds.

In the context of web apps, you can prepare Future responses this way and return them when they are ready. Works great with the async responses support from Java Servlets 3.0. Or with a framework such as Play Framework 2.x you can simply return Future[Response] straight from your controllers [3]

[1] http://docs.scala-lang.org/sips/pending/futures-promises.htm...

[2] http://doc.akka.io/docs/akka/snapshot/java/futures.html

[3] http://www.playframework.com/documentation/2.1.0/ScalaAsync

I suspect that you could so something very similar in C# - GetStringAsync returns a Task<string>, and you don't have to await it right away or one by one. So you can do:

   var urlTask1 = client.GetStringAsync(url1);
   var urlTask2 = client.GetStringAsync(url2);
   var firstDone = await Task.WhenAny(new Task[] { urlTask1, urlTask2 });
   
For the rest of it, see Task.ContinueWith, WhenAny, WhenAll etc. http://msdn.microsoft.com/en-us/library/dd235618.aspx

These also return Task<T> objects that you can work with further or await. I'd be very surprised if you can't do the equivalent, also in a totally non-blocking way.

.NET has futures and continuations, see Task[1].

I haven't used tasks heavily yet so I'm not sure how to implement everything in your example. But I believe it's all possible.

Async/await is "just" syntactic sugar for tasks. So I'm not sure what you mean by "still blocking from the perspective of the caller". When you call an async method, you get back a Task. When you use await on a task, the compiler rewrites your code to introduce continuations. If you need some more powerful Task features hidden by the syntactic sugar, you always still have the option of using tasks explicitly.

[1] http://msdn.microsoft.com/en-us/library/dd321424.aspx

The interesting thing about async / await is that for the coder, the code looks linear, but it doesn't execute that way. So a gap is opened up between "the perspective of the coder" and the perspective of the machine.

I think that bad_user means that if you await one client.GetStringAsync then await a second client.GetStringAsync, the second GetStringAsync only starts after the first one completes, so the first get "blocks" the second, "from the perspective of the caller". I.e no parallelism.

Of course, you don't have to do that. Code elsewhere in this thread.

It is neat that C# has some standard macros defined. It would be nicer if it was more explicit what is going on behind the scenes. I would love to see the equivalent of c-macro-expand. [0]

Also, although it is possible to extend C# using stuff like custom linq providers, [1] I still prefer lisp style macros.

[0] http://www.linuxjournal.com/article/2821 [1] http://msdn.microsoft.com/en-us/library/bb546158.aspx

Yep. The C# design attitude seems to be that the compiler-writers have the ability to define such macros, but the C# compiler users don't. Compare with F#.

Though that may change with C# 5 and Roslyn (http://en.wikipedia.org/wiki/Microsoft_Roslyn compiler as a service)

Surprisingly, you don't have to wait for Roslyn.

The technique exists now [0]. It is kind of a pain to use and seems kind of dangerous to use as a web app. But it is there. I messed with this a few years ago.

[0] http://stackoverflow.com/questions/3111190/compiling-net-cod...

"While async/await is a cool feature and is worth using, it is worth noting the up trend in complexity of generated code – and ansyc/await generates significantly more code in the simpler language without this feature than previous new features did."

This is the case with all high-level language features. Full lexical scoping with first class functions forces the PL implementation to either generate a <code;environment> tuple for all function values, or to perform aggressive closure conversion (up to Stalin levels) in addition. Virtual method dispatch gives rise to inline caches, often polymorphic ones. Lazy evaluation forces you to generate thunks. Pattern matching forces you to generate a decision tree or a similar structure. The list goes on. You already have such things in C#, even without async calls.

Yes,for each await it basically generates a state machine. It's pretty crazy to think how much code you don't have to write by using "await".
"yield return" also makes a state machine, but a simpler one. I like async/await, but I am aware of the steep upward trend in internal complexity of these features.

IMHO this means that the fruit of these features are no longer so low-hanging.