await/async vs. "classic" asynchronous (callbacks)
Asked Answered
C

2

13

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?
Conrado answered 24/10, 2011 at 0:33 Comment(3)
Which classic async model? There are three different ones.Anglocatholic
msdn.microsoft.com/en-us/magazine/hh456402.aspxAnglocatholic
@Anglocatholic the link is broken. I'm aware that 6 years have passed, but do you have an updated link?Amberambergris
C
17

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

Choric answered 24/10, 2011 at 11:38 Comment(0)
G
5

Like anonymous functions and iterators, the async and await keywords are syntactic sugar. In a technical sense, they are no more or less efficient than the equivalent non-sugary versions. They just save you a lot of typing.

Grigri answered 24/10, 2011 at 3:52 Comment(6)
Well, they do propagate the synchronization context (AFAIK), which has the potential to make them slightly slower if you're passing a lot of continuations that don't all need to run in the originating thread. It's possible to do that without await but it's not the default in ContinueWith.Neurovascular
Managing the synch and try/catch state is just part of the sugar.Grigri
Aaronaught: I think you're thinking of ConfigureAwait(...) which actually doesn't allow you to disable/enable synchronization propogation. What ConfigureAwait(...) allows you to do is decide if you want to use the sync context for scheduling. The design going forward (not yet released) will ensure that .NET execution context will always get packed/unpacked when a method gets packed up / restored due to an await.Choric
@TheoYaung: What I'm thinking of is the fact that if you want callbacks to run in the originating thread using System.Threading.Tasks, then you need to pass in TaskScheduler.FromCurrentSynchronizationContext as an argument to ContinueWith explicitly, whereas asynchronous sequences built using await will (ostensibly) always run its continuations in the same thread as the caller of the async method. Maybe I'm not correct about that; if so, would be nice to see some documentation or a blog entry, since that's what a lot of us have been led to believe...Neurovascular
@Aaronaught: The behavior you're describing is correct - here are the specifics :) (1) await reschedules back depending on the thing you're awaiting. (2) await on Task/Task<T> has a default behavior that schedules using 'SynchronizationContext.Post(...)'. Using 'ConfigureAwait(...)' on a Task can get you back to the TPL default behavior for ContinueWith(...).Choric
... The difference was that there was a strong desire to come back to the UI thread in the case of UI programming, and in those cases, they have sync contexts that do that. If your code is on a threadpool thread, await will come back to the first available threadpool thread (preserving throughput).Choric

© 2022 - 2024 — McMap. All rights reserved.