The zen of async: Best practices for best performance

Play The zen of async: Best practices for best performance
Sign in to queue


The new async support in C# and Visual Basic dramatically simplifies the process of building scalable and responsive apps for the client and cloud. However, while the programming model provided by this async support enables development in a style very reminiscent of synchronous programming, the required mental model and run time costs involved are anything but. In this session, we’ll dive deep into how async works, examining key aspects of the design and implementation that result in overheads when compared to synchronous programming. With that knowledge, we’ll tour through best practices for using async to build high-performing, scalable and responsive apps and libraries.







The Discussion

  • User profile image

    Awesome talk, Stephen! Wish I could have been there in person!

    Thank you in particular for going "deep".

    One quick question re your demo where Wait causes a deadlock: is that a change in behavior? I haven't tried it, but from my understanding of, I would expect Wait to run the task inline in this scenario.

  • User profile image

    Hi Stephen-

    Thanks, and I'm very glad to hear you enjoyed the talk!

    This isn't a change in behavior, rather this case just doesn't map to the same cases that would result in inlining.  In this case, there's actually nothing to be inlined: the task being returned from the async method doesn't have a delegate associated with it, so there's nothing to run. It's akin to waiting on a task created by a TaskCompletionSource<T>: you can use a TCS<T> to represent any arbitrary asynchronous operation, but when you Wait() on such a task, there's no invokable work associated with that Task<T>, so the Wait() call has no choice but to spin/block until the Task<T> is eventually signaled/completed.  The implementation currently has no way to associate the work being posted back to the UI thread with the task separately returned from the async method.


  • User profile image

    Excellent talk! Lots of extremely useful information presented very clearly.

  • User profile image

    Thanks, ermau.  I'm very glad you enjoyed it.

  • User profile image

    Thanks for the great talk! Very informative!

  • User profile image

    great talk.

    are there any plans to make MemoryStream.BeginRead synchronous like it was in .NET 1.1 ?

  • User profile image


  • User profile image

    Hi Piersh-

    MemoryStream.ReadAsync completes synchronously in .NET 4.5 Developer Preview.  MemoryStream doesn't have an override of BeginRead.

  • User profile image

    Is there still a chance that the unhandled exception change might be reverted to .NET 4 behavior?  This new behavior sounds quite scary.  In many (if not most) cases, if a Task is not observed nor waited upon, it is interesting for its side-effects.  Such side-effect may or may not have completed, or worse, have only partially completed when the task aborts due to an exception.

    It's clearly dev-unfriendly to observe that exception some non-deterministic (but potentially long) time later, but that's a hardly an argument for the change: it's much worse to get non-deterministic and potentially unnoticed state-corruption or deadlock.

    Furthermore, it's inconsisent with normal behavior and encourages bad habits.  You really don't want people throwing exceptions and ignoring them; that's the road to very-hard to debug code.  If such code were to be called in a synchronous fashion it would occasionally fail, leading to an unintuitive behavior difference - and one which encourages just adding a try with an empty catch block to the synchronous variant to maintain parity; a situation that makes the code much harder to maintain in the long run.

    Swallowing asynchronous exceptions by default strikes me as being a small, short-term gain in simple scenarios at the cost of higher maintenance costs in more realistic scenarios in the long run.

    A final small detail: In your web caching scenario, you don't add to the cache until the Task completes.  You could just use the more concise ConcurrentDictionaries GetOrAdd method with the added advantage of a much smaller window in which the cacher may make multiple identical requests.

  • User profile image

    emn13, thank you for the thoughtful feedback.  Regarding the exception behavior, nothing is set in stone for the release.  You can read more about this change here:

    Regarding ConcurrentDictionary.GetOrAdd, that won't do what I want in this case.  Note that I'm only storing the task into the dictionary if the task completes successfully... if I were to use GetOrAdd, the task would always end up getting stored, even if it faulted.  That's why I wait to store the task until the task has completed, so that I can store it conditionally.  You're right that if I were going to store it unconditionally, GetOrAdd would be a simpler approach.

  • User profile image
    Mikhail Mikheev

    Hi Stephen,

    I have a question about Task caching. When we return a cached Task instance and the client does continuation on it will the cached task create a reference to the continuation what prevents the continuation to be collected by GC until the cached task become unreachable?

    I've took a quick look at the Task implementation in .Net Reflector and it seems that when you do continuation on a Task instance the last one adds the continuation task into a collection. Why I care about it is in my scenario it would be usefull to cache tasks but as I do a lot of continuation (some of that include child task) would it mean that all continuation tasks lived in memmory as long as the cached task lived?

  • User profile image

    Hi Stephen.

    Thanks for the interesting and in depth talk.

    One question: How can one cache Tasks when they are Disposable? If the first caller disposes the resulting task (which is also cached), the second caller will get the same disposed task as result?

    On the topic of Disposing Tasks. That is seldom or never done in examples, even though Tasks implement the IDisposable interface. I couldn't find details on it in the MSDN documentation for the Task class and in the Task parallelism section either. Should one make sure to always dispose of received tasks, or is it unnecessary, and if so why are they IDisposable? If it is necessary, why is that seldom done in examples?


  • User profile image

    Hi Stephen,

    regarding the design of throwing exception in the finalizer thread on unobserved exceptions, this may well make sense but since it is potentially really dangerous, it needs to be much better highlighted and formalised in the MSDN documentation for the Task class and in the Task parallelism section.

    I think that a complex enough area would warrant a formal (check-)list of rules to adhere to, e.g..:

    • Tasks must always be observed. If a task is faulted and that is not observed, an exception will be thrown by the finalizer thread at a potentially much later point in time. For more details see http:/....
    • Tasks are disposables and thus needs to be disposed. (OR) Even though Tasks are disposables, they don't need to be disposed and doing so is a null operation.
    • ...



  • User profile image

    Mikhail, a task will drop references to its continuations when it completes. And if you register continuations with a task after that task has completed, the continuation is either immediately executed or scheduled, and no reference will be stored.

    Anders, Task's IDisposable implementation in .NET 4.5 exists purely to enable disposing of its WaitHandle, which is only ever allocated if you explicitly access the ((IAsyncResult)task).AsyncWaitHandle.  Even if disposed, the task will continue to work, except that trying to use its IAsyncResult.AsyncWaitHandle will result in an exception.  So, you should feel comfortable caching your own tasks, as the worst that will happen if someone disposes of them is that they won't be able to use what's effectively a legacy property (you shouldn't need to use this explicitly-implemented property unless you're bridging the gap with the existing IAsyncResult pattern).

    Anders, as mentioned in the talk, in .NET 4.5 tasks no longer throw their unobserved exception on the finalizer thread (you can re-enable the behavior with a configuration switch).  The .NET 4 behavior is discussed at, e.g. "If you do not wait on a task that propagates an exception, or access its Exception property, the exception is escalated according to the .NET exception policy when the task is garbage-collected."  Regarding disposing of Tasks, in general you shouldn't need to Dispose of tasks, and unless you can prove that it's actually beneficial, I'd urge you to forget that Task even implements IDisposable.  If we had it to do over again, I don't believe it would.

  • User profile image

    I did the same experiment in my dual core cpu i7-7600U, .NET Framework 4.5.2.
    And the results were disturbing.

    Sync Method: 0.03369
    Async Method: 39.34

    and similar difference in subsequent runs too.

    Any hint on why?

Add Your 2 Cents