Search code examples
c#asynchronousasync-ctpc#-5.0

await/async vs. "classic" asynchronous (callbacks)


So the new async CTP is very cool; it makes my life a lot easier not having to write named callback methods and makes the intent of the methods a lot clearer.

Now that I've gotten to play with it a little, I'm wondering what differences there may be between the async/await and the "classic" asynchronous callback syntaxes.

Here are a few questions I have in mind, but there are numerous others that I won't have thought of now and probably will later.

  • Does one perhaps offer superior performance over the other?
  • Is there an overhead to one that is greater than the other?
  • Which would be better to use in a high-performance environment?

Solution

  • The answer is complicated, the current compiler implementation of await is in several ways better than callbacks, but in some cases worse.

    .NET Execution Context: We intend for both await and ContinueWith(...) capture and restore .NET Execution Context. It wouldn't pass .NET safety requirements otherwise, because then you'd be able to take arbitrary things like credentials, etc. and leave them on the threadpool for the next workitem. For 'await', this is an adjustment we made in the internal builds, but it was after we produced the //BUILD developer preview.

    Memory Allocations: In several ways 'await' is better on memory allocations than manual callbacks. The key is that for functions with many awaits, what you're really generating is the equivalent of several callbacks. If you have 5 awaits in linear execution order, and with execution always flowing to the end, then the equivalent would require 5 callbacks. For each of those 5 callbacks, it's possible to generate a separate lambda closure object and a delegate that represents that specific lambda. In the 'await' case, the compiler knows that you're not going to use the delegate object for anything else. So instead, the entire method shares 1 closure and 1 delegate, with an internal state machine to keep track of where you are inside the method. Thus, for this case, 'await' allocates fewer objects, which actually can speed up your program since too many objects = more time the GC has to spend figuring out what's alive/dead.

    Short-cutting 'Await' also has fancier semantics than just callbacks. In the case where you are creating a callback lambda, the compiler is forced to allocate the closure and the lambda's entrypoint delegate no matter what. For 'await', the await contract allows for a more optimized codepath for awaitables that are already "done". if the awaitable says it's "done" before the await gets evaluated, the semantic is just a pure pass-through of yanking out the result. This means that there's an opportunity for the compiler to delay allocation until you really need it, and thus you never pay the closure allocation, delegate allocation, nor scheduling cost, unless you actually need it. The current Developer Preview compiler includes these performance optimizations.

    Trading danger for perf If you really want to bypass the .NET security model, you could kind of imagine a case where you can get a little bit of perf by avoiding the execution context package/restore, if you are absolutely confident that you will never need to capture/restore context. However, most of .NET's methods will do that silently under the covers, so you really need to know which ones will give you raw access without it. The rule of thumb for .NET is that if the API is available in partial trust (e.g. Silverlight), then the API certainly captures context when invoked, and then restores it, if it's an API that tranfers execution elsewhere (e.g. ContinueWith, QueueUserWorkItem(...), etc.). If you roll your own threadpool that just queues up delegates, you can bypass this, but most likely you don't need it.

    My personal recommendation Use await. It's higher level, and it's what you want. We've put in a fair amount of effort trying to tune it for this release, and we could probably tune it further. Callback-based APIs will be more limiting, because the compiler can only tune so much before they start breaking the language rules. Awaits in a method allow for you to have smarter closures than callbacks. AND... await is a lot more intuitive to read/use than callbacks :)