Jon Rumsey

An online markdown blog and knowledge repository.


Project maintained by nojronatron Hosted on GitHub Pages — Theme by mattgraham

Async-Await Learnings

Notes taken while working through Stephen Cleary's March 2013 article Async/Await - Best Practices in Asynchronous Programming.

Additional notes were taken while reading Task-based Asynchronous Programming Patterns from learn.microsoft.com.

Table of Contents

Best Practices Reading Overview

There are some basic rules of thumb to follow:

These are a mix of good ideas, not necessarily hard-and-fast rules, as there are details to each one depending on the DotNET version including ASP.NET, WinForms, WPF, etc.

Best Practices - Avoid Async Void

Async methods can return any of these three types:

Async Void should not be used except in the case of asynchronous Event Handlers:

Async Task methods are preferred when there is no return Type expected:

Generic Async Task methods provide the most capability:

public async void ChildMethodAsync()
{
  throw new InvalidOperationException();
}
public void ParentMethod()
{
  try
  {
    ChildMethodAsync();
  }
  catch (Exception)
  {
    // will not get caught!
    throw;
  }
}

In order to see these Exceptions while debugging, use AppDomain.UnhandledException (commonly available in WPF, ASP.NET).

A better approach:

// WPF event handler
private async void MyButton_OnClick(object sender, EventArgs e)
{
  await MyCustomMethodAsync();
}

// apply async Task to the custom method
public async Task MyCustomMethodAsync()
{
  // do awaitable work etc
  await Task.Run(()=> ...);
}

This provides:

When Using Lambdas, Anonymous Methods, and Other Delgates

Do not "...provide an async implementation (or override) to a void-returning method on an interface (or base class)":

"Lambdas should only be used if they're converted to a delegate type that returns Task (for example, Func<Task>)." To do this, encapsulate a void-return lambda expression in an Action/<T> delegate, similar to how a delegate can encapsulate a named method.

See .net 8 Action<T> for how to utilize Action<T> delegate.

Best Practices - Avoid Mixing Synchronous and Asynchronous

There are consequences to doing this:

Special Case: Console Applications

Thread Pool SynchronizationContext:

Warning: Moving asynchronous code from a Console Application to a GUI or ASP.NET application will require editing the code to avoid deadlocks.

Adding Async Code to Existing Codebase

Tasks Store Lists of Exceptions

When an Exception is thrown within an async (read: Task based) method:

Blocking Within Async Method

Methods that "aren't fully asynchronous" will not return a completed Task.

Async code should not include synchronous, blocking code.

An example as written in Async/Await Best Practices in Asynchronous Programming

public static class NotFullyAsynchronousDemo
{
  // This method synchronously blocks a thread
  public static async Task TestNotFullyAsync()
  {
    // Yield is an immediate-return statement (an incomplete Task)
    await Task.Yield();
    // Upon returning the next line of code will block the GUI or ASP.NET request thread
    Thread.Sleep(5000);
  }
}

The Async Way

To Retrieve the result of a background task: Do not use Task.Wait or Task.Result and instead use await

To Wait for any Task to complete: Do not use Task.WaitAny and instead use await Task.WhenAny

To Retrieve the results of multiple tasks: Do not use Task.WaitAll and instead use await Task.WhenAll

To Wait a period of time: Do not use Thread.Sleep and instead use await Task.Delay

Best Practices - Configure Context

Context is captured when an incomplete Task is awaited.

Captured context is used to resume the async method.

Resuming on the Context causes performance problems (slight, but they accumulate).

Advice: Await teh result of ConfigureAwait whenever possible.

ConfigureAwait

ConfigureAwait(bool) or ConfigureAwait(ConfigureAwaitOptions)

ConfigureAwaitOptions:

ConfigureAwait best practices:

Best Practices - Solutions To Common Async Problems

As written in Async/Await Best Practices in Asynchronous Programming:

Best Practices - Other Things To Keep Top Of Mind

About using ConfigureAwait vs using just await:

Controller Actions:

TAP - Common Classes

Task: A promise of eventual completion of an operation.

Task.ContinueWith(delegate callback): The callback to call when the Task is completed.

ValueTask<TResult>: A struct that can wrap a TResult or a Task<TResult> for return from an async method.

ValueTask: Non-generic version of ValueTask<T>:

Hot Paths:

Should every new method return ValueTask or ValueTask<TResult>?

Under what situation is is better to choose ValueResult over Task (or their generic counterparts)?

TAP - Overview

This is a brain-dump of concepts and mid-level details from the Summary chapter:

TAP - Generating Methods

Generating TAP Methods can be done three ways:

  1. Compiler: Use the 'async' keyword and returning a System.Threading.Tasks.Task or ...Task<TResult>. Exceptions are packaged in the Task object and TaskStatus.Faulted state is set. OperationCanceledException that are UNHANDLED results in TResult Task.Canceled state.
  2. Manually: Provides better control over implementation. Use System.Threading.Tasks and System.Runtime.CompilerService namespaces. Create a TaskCompletionSource<TResult> object that calls SetResult when completed, or SetException when faulted, or SetCanceled if canceled by token (TrySetException, TrySetException, and TrySetCanceled, accordingly).
  3. Hybrid: Implement TAP pattern manually but delegate core logic to the compiler. Use this to validate arguments and catch Exceptions outside of the Compiler-generated async Task implementation. In essence: Ensure Exceptions are thrown back to the calling function, rather than wrapped in a System.Threading.Tasks.Task object. This pattern can also be used to "return a cached Task".

Example MANUAL generation of a TAP method:

// Manually generating a TAP method
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
  // Quote: When you implement a TAP method manually
  // you must complete the resulting task when the
  // represented asynchronous operation completes.
    var tcs = new TaskCompletionSource<int>();
    stream.BeginRead(buffer, offset, count, ar =>
    {
        try 
    { 
      tcs.SetResult(stream.EndRead(ar)); 
    }
        catch (Exception exc) 
    { 
      tcs.SetException(exc); 
    }
    }, state);
    return tcs.Task;
}

Example HYBRID TAP method generation:

// Hybrid TAP method generation
public Task<int> MethodAsync(string input)
{
    if (input == null) throw new ArgumentNullException("input");
    return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)
{
   // code that uses await goes here
   return value;
}

TAP - IO and Compute Workloads

IO Workloads

Includes asynchronous methods that are awaiting (a usually remote) process to return results, but not utilizing compute.

Ensure these are set to be asynchronous:

Compute Workloads

Includes asynchronous methods that are performing the computationally heavy processing.

Although these shouldn't be exposed publicly from the library, if they are they should be exposed as Synchronous (NOT async).

The implementation Library can decide whether to offload them to another thread or execute them in parallel.

Common Compute and IO Implementations

Task.Run() and TaskFactory.StartNew():

Task.ContinueWith():

TaskFactory.ContinueWhenAll() and TaskFactory.ContinueWhenAny():

In a Compute-bound Task:

Hybrid Workloads: Compute-bound and IO-bound:

TAP - Consuming TAP

If SynchronizationContext object is associated with thread that executed async method when suspended:

Configure Suspend/Resume with Yield and ConfigureAwait:

// do not use this if it is important to return
// to a specific context like the UI thread
await someTask.ConfigureAwait(continueOnCapturedContext:false);

TAP - Canceling Async Operations

Canceling Async Operation:

Note: It is also possible for a Task to "self cancel" by calling the passed-in CancellationToken and calling its Cancel() method!

TAP - Monitoring Async Operation Progress

Monitoring Progress:

// consume this method by WPF to track download
// progress initiated by a button event handler
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

TAP - Combinators

Built-in Task-based Combinators:

About Task WhenAll

If a try-catch block wraps an await Task.WhenAll() operation, the Exceptions are consolidated into AggregateException.

Unwrap AggregateException by performing a foreach in the catch block and iterate through Task items selecting by Task.IsFaulted and capture each Task Exception using Task.Exception property.

About Task Delay

Introduce pauses into async method execution:

Caveats:

TAP - Building Task-based Combinators

Several Combinators are included in the .NET Libraries, but custom Combinators can be designed and implemented:

In either of the examples in subsection RetryOnDefault, the custom methods attempt to return a function (awaited function using TAP) that might throw. A catch statement tests the number of times the attempt has been made (1 to maxRetries) and will throw on the last retry, else will iterate until last retry then return default(t).

Other ideas include:

Custom Combinator Example: NeedOnlyOne

If calling multiple APIs but a response from only 1 is needed, pick the 1st one to return and cancel the remaining calls, and return only the 1st completed call.

Overview (see the actual page for the code):

  1. Accept a Func<T> as an array of Cancellation Token and Task<T> instances ([] functions).
  2. Iterate through each function in functions and assign them to new Task[] array.
  3. Call await Task.WhenAny(collectionOfFunctionInstances).ConfigureAwait(false) and assign the result to a variable e.g. completed.
  4. Call cancellationToken.Cancel.
  5. Iterate through each Task in tasks array and implement TaskContinuationOptions.OnlyOnFaulted, assigning them to a variable e.g. ignored.
  6. Return completed variable (which is a Task).

Custom Combinator Example: Interleaved Operations

WhenAny() might introduce performance problems because it registers a Continuation with each Task.

Following a technique as explained in Interleaved Operations can help to avoid issues with performance and handling completed and/or failed tasks.

Note: Thread safety is important here and using the Interlocked class might be necessary to avoid preemptive overwriting of an instance variable (for example).

Another possible custom Combinator is Task<T> WhenAllOrFirstException(IEnumerable<Task<T>>):

Building Task-Based Data Structures

Leverage Task and Task<TResult> with data structures in an async-compatible way.

AsyncCache:

The article demonstrates a custom Class of type TKey, TValue (a dictionary or KVP), where an internal ConcurrentDictionary<Tkey, Lazy<Task<TValue>>> maintains values registered via the CTOR, and accessed using an indexer method. Accepting a Func<TKey, Task<TValue>> valueFactory delegate ensures previously-added KVPs are rapidly found and a cahced copy can be returned using key. Since this is built on top of System.Threading.Tasks.Task it is capable of handling concurrent operations (multiple key access).

The Async Producer Consumer Collection:

An example as written in the Asunc Producer Consumer Collection (linked above):

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

Now leverage the above code:

private static AsyncProducerConsumerCollection<int> m_data = ;

// additional fields, properties

private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}

// additional members

private static void Produce(int data)
{
    m_data.Add(data);
}

With BufferBlock<T>:

private static BufferBlock<int> m_data = ; // some value

// other fields and props...

private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}

// other members here

private static void Produce(int data)
{
    m_data.Post(data);
}

Valid Consumption Patterns for ValueTasks

This section contains notes gleaned from DevBlogs.Microsoft.com: Understanding the whys whats and whens of valuetask.

Limited Surface Area

ValueTask and ValueTask<TResult> are:

NEVER do the following with ValueTask or ValueTask<TResult>:

Instead, use AsTask() or simply await or ConfigureAwait(false).

Using AsTask:

  1. Use .AsTask()
  2. Operate on the resulting Task or Task<TResult>.
  3. Never interact with the current ValueTask or ValueTask<TResult> again.

Examples using await, ConfigureAwait(false), and AsTask():

BENEFITS of using Task and Task<TResult>:

Hanselman and Toub on Async Await

Asynchrony Breeds Concurrency

Quoted from Stephen Toub.

Delegates

Delegates in C# are 'managed function pointers'.

Tasks

Consider Tasks to be Action<T> delegates, but with super powers:

Note about lock on this: First off do not do it. A lock is 'private state', but 'this' is public. This enables code to access private state. If no other code could get a handle on the lock (this) code then it might be safe. However, a public class that when instantiated basically ends up with a public this, locking the private state is now accessible to other callers.

Key takeaway:

Exceptions in Tasks

Recall that an Exception instance can be thrown.

In .NET 4.0 the proper 'rethrow' implementation was to add the _exception instance as an InnerException, so that all Watson Trace information is included.

// class and function code...
if (_exception is not null)
{
  throw new Exception("comment", _exception); // e.g. (string command, Exception innerException)
}
// other code...

Tasks can represent multiple operations!

AggregateException helps with this by allowing multiple Exceptions to be automatically wrapped without losing the Watson/StackTrace details (reducing code implementation by developers).

Exception Dispatch Info

This is directly related to how Exceptions within Tasks are (and should be) handled:

Foreground and Background Threads

When Main() method exits, should it wait for all the Threads to complete before exiting?

Use of Interlocked

Avoids the problem of using lock() on a parallel set of operations:

Thread Sleep

This affects the Thread Pool entirely!

Iterators vs Async Await

Stephen Toub stated the following:

Things To Review

If cancellation is requested but a result or an exception is still produced, the task should end in the RanToCompletion or Faulted state.

A TAP method may even have nothing to execute, and may just return a Task that represents the occurrence of a condition elsewhere in the system (for example, a task that represents data arriving at a queued data structure).

The Producer-Consumer Collection pattern!

NuGet Package System.Threading.Tasks.Dataflow.

Resources

Asynchronout Programming Scenarios.

Async/Await - Best Practices in Asynchronous Programming

Stephen Cleary adds a great response to a StackOverflow question that is worth reviewing, especially in the context of (now legacy) ASP.NET.

Read more about the Task-based Asynchrounous Pattern TAP in .NET: Introduction and overview on MSDN.

MSFT DevBlogs Understanding The Whys Whats And Whens Of ValueTask.

Stephen Toub, Partner Software Engineer talks with Scott Hanselman about writing async-await from scratch in C#.

Return to Conted Index

Return to Root README