Edit

Consuming the Task-based Asynchronous Pattern

When you use the Task-based Asynchronous Pattern (TAP) to work with asynchronous operations, you can use callbacks to achieve waiting without blocking. For tasks, this pattern uses methods such as Task.ContinueWith. Language-based asynchronous support hides callbacks by allowing asynchronous operations to be awaited within normal control flow, and compiler-generated code provides this same API-level support.

Suspending Execution with Await

You can use the await keyword in C# and the Await Operator in Visual Basic to asynchronously await Task and Task<TResult> objects. When you await a Task, the await expression is of type void. When you await a Task<TResult>, the await expression is of type TResult. An await expression must occur inside the body of an asynchronous method. (These language features were introduced in .NET Framework 4.5.)

Under the covers, the await functionality installs a callback on the task by using a continuation. This callback resumes the asynchronous method at the point of suspension. When the asynchronous method is resumed, if the awaited operation completed successfully and was a Task<TResult>, its TResult is returned. If the Task or Task<TResult> that was awaited ended in the Canceled state, an OperationCanceledException exception is thrown. If the Task or Task<TResult> that was awaited ended in the Faulted state, the exception that caused it to fault is thrown. A Task can fault as a result of multiple exceptions, but only one of these exceptions is propagated. However, the Task.Exception property returns an AggregateException exception that contains all the errors.

If a synchronization context (SynchronizationContext object) is associated with the thread that was executing the asynchronous method at the time of suspension (for example, if the SynchronizationContext.Current property is not null), the asynchronous method resumes on that same synchronization context by using the context's Post method. Otherwise, it relies on the task scheduler (TaskScheduler object) that was current at the time of suspension. Typically, this is the default task scheduler (TaskScheduler.Default), which targets the thread pool. This task scheduler determines whether the awaited asynchronous operation should resume where it completed or whether the resumption should be scheduled. The default scheduler typically allows the continuation to run on the thread that the awaited operation completed.

When you call an asynchronous method, it synchronously executes the body of the function up until the first await expression on an awaitable instance that isn't yet complete, at which point the invocation returns to the caller. If the asynchronous method doesn't return void, it returns a Task or Task<TResult> object to represent the ongoing computation. In a non-void asynchronous method, if a return statement is encountered or the end of the method body is reached, the task is completed in the RanToCompletion final state. If an unhandled exception causes control to leave the body of the asynchronous method, the task ends in the Faulted state. If that exception is an OperationCanceledException, the task instead ends in the Canceled state. In this manner, the result or exception is eventually published.

Several important variations of this behavior exist. For performance reasons, if a task is already complete by the time the task is awaited, control isn't yielded, and the function continues to execute. Additionally, returning to the original context isn't always the desired behavior and can be changed; this behavior is described in more detail in the next section.

Configuring Suspension and Resumption with Yield and ConfigureAwait

Several methods provide more control over an asynchronous method's execution. For example, you can use the Task.Yield method to introduce a yield point into the asynchronous method:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

This method is equivalent to asynchronously posting or scheduling back to the current context.

public static async Task YieldLoopExample()
{
    await Task.Run(async delegate
    {
        for (int i = 0; i < 1000000; i++)
        {
            await Task.Yield(); // fork the continuation into a separate work item
        }
    });
}
Public Async Function YieldLoopExample() As Task
    Await Task.Run(Async Function()
                       For i As Integer = 0 To 999999
                           Await Task.Yield() ' fork the continuation into a separate work item
                       Next
                   End Function)
End Function

You can also use the Task.ConfigureAwait method for better control over suspension and resumption in an asynchronous method. As mentioned previously, by default, the current context is captured at the time an asynchronous method is suspended, and that captured context is used to invoke the asynchronous method's continuation upon resumption. In many cases, this is the exact behavior you want. In other cases, you might not care about the continuation context, and you can achieve better performance by avoiding such posts back to the original context. To enable this behavior, use the Task.ConfigureAwait method to inform the await operation not to capture and resume on the context, but to continue execution wherever the asynchronous operation that was being awaited completed:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Awaitables, ConfigureAwait, and SynchronizationContext

await works with any type that satisfies the awaitable expression pattern, not just Task. A type is awaitable if it provides a compatible GetAwaiter method that returns a type with IsCompleted, OnCompleted, and GetResult members. In most public APIs, return Task, Task<TResult>, ValueTask, or ValueTask<TResult>. Use custom awaitables only for specialized scenarios.

Use ConfigureAwait when the continuation doesn't need the caller's context. In app code that updates a UI, context capture is often required. In reusable library code, ConfigureAwait(false) is usually preferred because it avoids unnecessary context hops and reduces deadlock risk for callers that block.

ConfigureAwait(false) changes continuation scheduling, not ExecutionContext flow. For a deeper explanation of context behavior, see ExecutionContext and SynchronizationContext.

Canceling an asynchronous operation

Starting with .NET Framework 4, TAP methods that support cancellation provide at least one overload that accepts a cancellation token (CancellationToken object).

You create a cancellation token through a cancellation token source (CancellationTokenSource object). The source's Token property returns the cancellation token that signals when the source's Cancel method is called.

var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

For example, if you want to download a single webpage and you want to be able to cancel the operation, create a CancellationTokenSource object, pass its token to the TAP method, and then call the source's Cancel method when you're ready to cancel the operation:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

Or, you can pass the same token to a selective subset of operations:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

Important

Any thread can initiate cancellation requests.

You can pass the CancellationToken.None value to any method that accepts a cancellation token to indicate that cancellation is never requested. This value causes the CancellationToken.CanBeCanceled property to return false, and the called method can optimize accordingly. For testing purposes, you can also pass in a pre-canceled cancellation token that is instantiated by using the constructor that accepts a Boolean value to indicate whether the token should start in an already-canceled or not-cancelable state.

This approach to cancellation has several advantages:

  • You can pass the same cancellation token to any number of asynchronous and synchronous operations.

  • The same cancellation request can go to any number of listeners.

  • The developer of the asynchronous API has complete control over whether cancellation can be requested and when it takes effect.

  • The code that consumes the API can selectively determine the asynchronous invocations that cancellation requests go to.

Monitoring progress

Some asynchronous methods expose progress through a progress interface that you pass into the asynchronous method. For example, consider a function that asynchronously downloads a string of text, and along the way raises progress updates that include the percentage of the download that has completed thus far. You can consume such a method in a Windows Presentation Foundation (WPF) application as follows:

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; }
}

Using the built-in task-based combinators

The System.Threading.Tasks namespace includes several methods for composing and working with tasks.

Note

Several code samples in this section use Bitmap, which requires the System.Drawing.Common package and is supported only on Windows. The async patterns they demonstrate apply on all platforms; substitute a cross-platform imaging library for non-Windows targets.

Task.Run

The Task class includes several Run methods that let you easily offload work as a Task or Task<TResult> to the thread pool. For example:

public static async Task TaskRunBasicExample()
{
    int answer = 42;
    string result = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer.ToString();
    });
    Console.WriteLine(result);
}
Public Async Function TaskRunBasicExample() As Task
    Dim answer As Integer = 42
    Dim result As String = Await Task.Run(Function()
                                              ' … do compute-bound work here
                                              Return answer.ToString()
                                          End Function)
    Console.WriteLine(result)
End Function

Some of these Run methods, such as the Task.Run(Func<Task>) overload, exist as shorthand for the TaskFactory.StartNew method. This overload enables you to use await within the offloaded work. For example:

public static async Task TaskRunAsyncExample()
{
    Bitmap image = await Task.Run(async () =>
    {
        using Bitmap bmp1 = await Stubs.DownloadFirstImageAsync();
        using Bitmap bmp2 = await Stubs.DownloadSecondImageAsync();
        return Stubs.Mashup(bmp1, bmp2);
    });
}
Public Async Function TaskRunAsyncExample() As Task
    Dim image As Bitmap = Await Task.Run(Async Function()
                                             Using bmp1 As Bitmap = Await Stubs.DownloadFirstImageAsync()
                                                 Using bmp2 As Bitmap = Await Stubs.DownloadSecondImageAsync()
                                                     Return Stubs.Mashup(bmp1, bmp2)
                                                 End Using
                                             End Using
                                         End Function)
End Function

Such overloads are logically equivalent to using the TaskFactory.StartNew method in conjunction with the Unwrap extension method in the Task Parallel Library.

Task.FromResult

Use the FromResult method in scenarios where data might already be available and you just need to return it from a task-returning method lifted into a Task<TResult>:

public static Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return Stubs.TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal(key);
}

static async Task<int> GetValueAsyncInternal(string key)
{
    await Task.Delay(1);
    return 0;
}
Public Function GetValueAsync(key As String) As Task(Of Integer)
    Dim cachedValue As Integer
    If Stubs.TryGetCachedValue(cachedValue) Then
        Return Task.FromResult(cachedValue)
    Else
        Return GetValueAsyncInternal(key)
    End If
End Function

Private Async Function GetValueAsyncInternal(key As String) As Task(Of Integer)
    Await Task.Delay(1)
    Return 0
End Function

Task.WhenAll

Use the WhenAll method to asynchronously wait on multiple asynchronous operations that are represented as tasks. The method has multiple overloads that support a set of non-generic tasks or a non-uniform set of generic tasks (for example, asynchronously waiting for multiple void-returning operations, or asynchronously waiting for multiple value-returning methods where each value might have a different type) and to support a uniform set of generic tasks (such as asynchronously waiting for multiple TResult-returning methods).

Suppose you want to send email messages to several customers. You can overlap sending the messages so you're not waiting for one message to complete before sending the next. You can also find out when the send operations complete and whether any errors occur:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

This code doesn't explicitly handle exceptions that might occur, but it lets exceptions propagate out of the await on the resulting task from WhenAll. To handle the exceptions, use code such as the following:

public static async Task WhenAllWithCatch()
{
    IEnumerable<Task> asyncOps = from addr in Stubs.addrs select Stubs.SendMailAsync(addr);
    try
    {
        await Task.WhenAll(asyncOps);
    }
    catch (Exception exc)
    {
        Console.WriteLine(exc);
    }
}
Public Async Function WhenAllWithCatch() As Task
    Dim asyncOps As IEnumerable(Of Task) = From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)
    Try
        Await Task.WhenAll(asyncOps)
    Catch exc As Exception
        Console.WriteLine(exc)
    End Try
End Function

In this case, if any asynchronous operation fails, all the exceptions are consolidated in an AggregateException exception, which is stored in the Task that is returned from the WhenAll method. However, only one of those exceptions is propagated by the await keyword. If you want to examine all the exceptions, you can rewrite the previous code as follows:

public static async Task WhenAllExamineExceptions()
{
    Task[] asyncOps = (from addr in Stubs.addrs select Stubs.SendMailAsync(addr)).ToArray();
    try
    {
        await Task.WhenAll(asyncOps);
    }
    catch (Exception exc)
    {
        foreach (Task faulted in asyncOps.Where(t => t.IsFaulted))
        {
            Console.WriteLine($"Faulted: {faulted.Exception}");
        }
    }
}
Public Async Function WhenAllExamineExceptions() As Task
    Dim asyncOps As Task() = (From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)).ToArray()
    Try
        Await Task.WhenAll(asyncOps)
    Catch exc As Exception
        For Each faulted As Task In asyncOps.Where(Function(t) t.IsFaulted)
            Console.WriteLine($"Faulted: {faulted.Exception}")
        Next
    End Try
End Function

Consider an example of downloading multiple files from the web asynchronously. In this case, all the asynchronous operations have homogeneous result types, and it's easy to access the results:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringTaskAsync(url));

You can use the same exception-handling techniques discussed in the previous void-returning scenario:

public static async Task WhenAllDownloadPagesExceptions()
{
    Task<string>[] asyncOps =
        (from url in Stubs.urls select Stubs.DownloadStringTaskAsync(url)).ToArray();
    try
    {
        string[] pages = await Task.WhenAll(asyncOps);
        Console.WriteLine(pages.Length);
    }
    catch (Exception exc)
    {
        foreach (Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
        {
            Console.WriteLine($"Faulted: {faulted.Exception}");
        }
    }
}
Public Async Function WhenAllDownloadPagesExceptions() As Task
    Dim asyncOps As Task(Of String)() =
        (From url In Stubs.urls Select Stubs.DownloadStringTaskAsync(url)).ToArray()
    Try
        Dim pages As String() = Await Task.WhenAll(asyncOps)
        Console.WriteLine(pages.Length)
    Catch exc As Exception
        For Each faulted As Task(Of String) In asyncOps.Where(Function(t) t.IsFaulted)
            Console.WriteLine($"Faulted: {faulted.Exception}")
        Next
    End Try
End Function

Task.WhenAny

Use the WhenAny method to asynchronously wait for just one of multiple asynchronous operations represented as tasks to complete. This method serves four primary use cases:

  • Redundancy: Performing an operation multiple times and selecting the one that completes first (for example, contacting multiple stock quote web services that return a single result and selecting the one that completes the fastest).

  • Interleaving: Launching multiple operations and waiting for all of them to complete, but processing them as they complete.

  • Throttling: Allowing additional operations to begin as others complete. This scenario is an extension of the interleaving scenario.

  • Early bailout: For example, an operation represented by task t1 can be grouped in a WhenAny task with another task t2, and you can wait on the WhenAny task. Task t2 could represent a time-out, or cancellation, or some other signal that causes the WhenAny task to complete before t1 completes.

Redundancy

Consider a case where you want to make a decision about whether to buy a stock. Several stock recommendation web services exist that you trust, but depending on daily load, each service can end up being slow at different times. Use the WhenAny method to receive a notification when any operation completes:

public static async Task WhenAnyRedundancy(string symbol)
{
    var recommendations = new List<Task<bool>>()
    {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    };
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyRedundancy(symbol As String) As Task
    Dim recommendations As New List(Of Task(Of Boolean)) From {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    }
    Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
    If Await recommendation Then Stubs.BuyStock(symbol)
End Function

Unlike WhenAll, which returns the unwrapped results of all tasks that completed successfully, WhenAny returns the task that completed. If a task fails, it's important to know that it failed, and if a task succeeds, it's important to know which task the return value is associated with. Therefore, you need to access the result of the returned task, or further await it, as this example shows.

As with WhenAll, you have to be able to accommodate exceptions. Because you receive the completed task back, you can await the returned task to have errors propagated, and try/catch them appropriately; for example:

public static async Task WhenAnyRetryOnException(string symbol)
{
    Task<bool>[] allRecommendations = new Task<bool>[]
    {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    };
    var remaining = allRecommendations.ToList();
    while (remaining.Count > 0)
    {
        Task<bool> recommendation = await Task.WhenAny(remaining);
        try
        {
            if (await recommendation) Stubs.BuyStock(symbol);
            break;
        }
        catch (WebException)
        {
            remaining.Remove(recommendation);
        }
    }
}
Public Async Function WhenAnyRetryOnException(symbol As String) As Task
    Dim allRecommendations As Task(Of Boolean)() = {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    }
    Dim remaining As List(Of Task(Of Boolean)) = allRecommendations.ToList()
    While remaining.Count > 0
        Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(remaining)
        Try
            If Await recommendation Then Stubs.BuyStock(symbol)
            Exit While
        Catch ex As WebException
            remaining.Remove(recommendation)
        End Try
    End While
End Function

Additionally, even if a first task completes successfully, subsequent tasks might fail. At this point, you have several options for dealing with exceptions: You can wait until all the launched tasks complete, in which case you can use the WhenAll method, or you can decide that all exceptions are important and must be logged. For this scenario, you can use continuations to receive a notification when tasks complete asynchronously:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

or:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

or even:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach (var task in tasks)
    {
        try { await task; }
        catch (Exception exc) { Stubs.Log(exc); }
    }
}
Private Async Sub LogCompletionIfFailed(tasks As IEnumerable(Of Task))
    For Each task In tasks
        Try
            Await task
        Catch exc As Exception
            Stubs.Log(exc)
        End Try
    Next
End Sub

Finally, you might want to cancel all the remaining operations:

public static async Task WhenAnyCancelRemainder(string symbol)
{
    var cts = new CancellationTokenSource();
    var recommendations = new List<Task<bool>>()
    {
        Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
    };

    Task<bool> recommendation = await Task.WhenAny(recommendations);
    cts.Cancel();
    if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyCancelRemainder(symbol As String) As Task
    Dim cts As New CancellationTokenSource()
    Dim recommendations As New List(Of Task(Of Boolean)) From {
        Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
    }

    Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
    cts.Cancel()
    If Await recommendation Then Stubs.BuyStock(symbol)
End Function

Interleaving

Consider a case where you're downloading images from the web and processing each image (for example, adding the image to a UI control). You process the images sequentially on the UI thread, but want to download the images as concurrently as possible. Also, you don't want to hold up adding the images to the UI until they're all downloaded. Instead, you want to add them as they complete.

public static async Task WhenAnyInterleaving(string[] imageUrls)
{
    List<Task<Bitmap>> imageTasks =
        (from imageUrl in imageUrls select Stubs.GetBitmapAsync(imageUrl)).ToList();
    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch { }
    }
}
Public Async Function WhenAnyInterleaving(imageUrls As String()) As Task
    Dim imageTasks As List(Of Task(Of Bitmap)) =
        (From imageUrl In imageUrls Select Stubs.GetBitmapAsync(imageUrl)).ToList()
    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch
        End Try
    End While
End Function

You can also apply interleaving to a scenario that involves computationally intensive processing on the ThreadPool of the downloaded images; for example:

public static async Task WhenAnyInterleavingWithProcessing(string[] imageUrls)
{
    List<Task<Bitmap>> imageTasks =
        (from imageUrl in imageUrls
         select Stubs.GetBitmapAsync(imageUrl)
             .ContinueWith(t => Stubs.ConvertImage(t.Result))).ToList();
    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch { }
    }
}
Public Async Function WhenAnyInterleavingWithProcessing(imageUrls As String()) As Task
    Dim imageTasks As List(Of Task(Of Bitmap)) =
        (From imageUrl In imageUrls
         Select Stubs.GetBitmapAsync(imageUrl).ContinueWith(Function(t) Stubs.ConvertImage(t.Result))).ToList()
    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch
        End Try
    End While
End Function

Throttling

Consider the interleaving example, except that the user is downloading so many images that the downloads have to be throttled. For example, you want only a specific number of downloads to happen concurrently. To achieve this goal, start a subset of the asynchronous operations. As operations complete, you can start additional operations to take their place:

public static async Task WhenAnyThrottling(Uri[] uriList)
{
    const int CONCURRENCY_LEVEL = 15;
    int nextIndex = 0;
    var imageTasks = new List<Task<Bitmap>>();
    while (nextIndex < CONCURRENCY_LEVEL && nextIndex < uriList.Length)
    {
        imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
        nextIndex++;
    }

    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch (Exception exc) { Stubs.Log(exc); }

        if (nextIndex < uriList.Length)
        {
            imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
            nextIndex++;
        }
    }
}
Public Async Function WhenAnyThrottling(uriList As Uri()) As Task
    Const CONCURRENCY_LEVEL As Integer = 15
    Dim nextIndex As Integer = 0
    Dim imageTasks As New List(Of Task(Of Bitmap))
    While nextIndex < CONCURRENCY_LEVEL AndAlso nextIndex < uriList.Length
        imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
        nextIndex += 1
    End While

    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch exc As Exception
            Stubs.Log(exc)
        End Try

        If nextIndex < uriList.Length Then
            imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
            nextIndex += 1
        End If
    End While
End Function

Early Bailout

Consider that you're waiting asynchronously for an operation to complete while simultaneously responding to a user's cancellation request (for example, the user clicked a cancel button). The following code illustrates this scenario:

class EarlyBailoutUI
{
    private CancellationTokenSource? m_cts;

    public void btnCancel_Click(object sender, EventArgs e)
    {
        if (m_cts != null) m_cts.Cancel();
    }

    public async void btnRun_Click(object sender, EventArgs e)
    {
        m_cts = new CancellationTokenSource();
        try
        {
            Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url");
            await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
            if (imageDownload.IsCompleted)
            {
                Bitmap image = await imageDownload;
                Stubs.Log(image);
            }
            else imageDownload.ContinueWith(t => Stubs.Log(t));
        }
        finally { }
    }
}
Class EarlyBailoutUI
    Private m_cts As CancellationTokenSource

    Public Sub btnCancel_Click(sender As Object, e As EventArgs)
        If m_cts IsNot Nothing Then m_cts.Cancel()
    End Sub

    Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
        m_cts = New CancellationTokenSource()
        Try
            Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url")
            Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
            If imageDownload.IsCompleted Then
                Dim image As Bitmap = Await imageDownload
                Stubs.Log(image)
            Else
                imageDownload.ContinueWith(Sub(t) Stubs.Log(t))
            End If
        Finally
        End Try
    End Sub
End Class

This implementation re-enables the user interface as soon as you decide to bail out, but doesn't cancel the underlying asynchronous operations. Another alternative would be to cancel the pending operations when you decide to bail out, but not reestablish the user interface until the operations complete, potentially due to ending early due to the cancellation request:

class EarlyBailoutWithTokenUI
{
    private CancellationTokenSource? m_cts;

    public async void btnRun_Click(object sender, EventArgs e)
    {
        m_cts = new CancellationTokenSource();
        try
        {
            Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url", m_cts.Token);
            await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
            Bitmap image = await imageDownload;
            Stubs.Log(image);
        }
        catch (OperationCanceledException) { }
        finally { }
    }
}
Class EarlyBailoutWithTokenUI
    Private m_cts As CancellationTokenSource

    Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
        m_cts = New CancellationTokenSource()
        Try
            Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url", m_cts.Token)
            Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
            Dim image As Bitmap = Await imageDownload
            Stubs.Log(image)
        Catch ex As OperationCanceledException
        Finally
        End Try
    End Sub
End Class

Another example of early bailout involves using the WhenAny method in conjunction with the Delay method, as discussed in the next section.

Task.Delay

Use the Task.Delay method to add pauses into an asynchronous method's execution. This pause is useful for many kinds of functionality, including building polling loops and delaying the handling of user input for a predetermined period of time. You can also use the Task.Delay method with Task.WhenAny to implement time-outs on awaits.

If a task that's part of a larger asynchronous operation (for example, an ASP.NET web service) takes too long to complete, the overall operation could suffer, especially if it fails to ever complete. For this reason, it's important to be able to time out when waiting on an asynchronous operation. The synchronous Task.Wait, Task.WaitAll, and Task.WaitAny methods accept time-out values, but the corresponding TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny and the previously mentioned Task.WhenAll/Task.WhenAny methods don't. Instead, use Task.Delay and Task.WhenAny together to implement a time-out.

For example, in your UI application, suppose that you want to download an image and disable the UI while the image is downloading. However, if the download takes too long, you want to re-enable the UI and discard the download:

public static async Task<Bitmap?> DownloadWithTimeout(string url)
{
    Task<Bitmap> download = Stubs.GetBitmapAsync(url);
    if (download == await Task.WhenAny(download, Task.Delay(3000)))
    {
        return await download;
    }
    else
    {
        var ignored = download.ContinueWith(
            t => Trace($"Task finally completed: {t.Status}"));
        return null;
    }
}

static void Trace(string message) => Console.WriteLine(message);
Public Async Function DownloadWithTimeout(url As String) As Task(Of Bitmap)
    Dim download As Task(Of Bitmap) = Stubs.GetBitmapAsync(url)
    If download Is Await Task.WhenAny(download, Task.Delay(3000)) Then
        Return Await download
    Else
        Dim ignored = download.ContinueWith(Sub(t) TraceMsg($"Task finally completed: {t.Status}"))
        Return Nothing
    End If
End Function

The same principle applies to multiple downloads, because WhenAll returns a task:

public static async Task<Bitmap[]?> DownloadMultipleWithTimeout(string[] imageUrls)
{
    Task<Bitmap[]> downloads =
        Task.WhenAll(from url in imageUrls select Stubs.GetBitmapAsync(url));
    if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
    {
        return await downloads;
    }
    else
    {
        downloads.ContinueWith(t => Stubs.Log(t));
        return null;
    }
}
Public Async Function DownloadMultipleWithTimeout(imageUrls As String()) As Task(Of Bitmap())
    Dim downloads As Task(Of Bitmap()) =
        Task.WhenAll(From url In imageUrls Select Stubs.GetBitmapAsync(url))
    If downloads Is Await Task.WhenAny(downloads, Task.Delay(3000)) Then
        Return Await downloads
    Else
        downloads.ContinueWith(Sub(t) Stubs.Log(t))
        Return Nothing
    End If
End Function

Building Task-based Combinators

Because a task is able to completely represent an asynchronous operation and provide synchronous and asynchronous capabilities for joining with the operation, retrieving its results, and so on, you can build useful libraries of combinators that compose tasks to build larger patterns. As discussed in the previous section, .NET includes several built-in combinators, but you can also build your own. The following sections provide several examples of potential combinator methods and types.

RetryOnFault

In many situations, you want to retry an operation if a previous attempt fails. For synchronous code, you might build a helper method such as RetryOnFault in the following example to accomplish this task:

public static T RetryOnFault<T>(Func<T> function, int maxTries)
{
    for (int i = 0; i < maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries - 1) throw; }
    }
    return default(T)!;
}
Public Function RetryOnFaultSync(Of T)(func As Func(Of T), maxTries As Integer) As T
    For i As Integer = 0 To maxTries - 1
        Try
            Return func()
        Catch
            If i = maxTries - 1 Then Throw
        End Try
    Next
    Return Nothing
End Function

You can build an almost identical helper method for asynchronous operations that are implemented with TAP and thus return tasks:

public static async Task<T> RetryOnFault<T>(Func<Task<T>> function, int maxTries)
{
    for (int i = 0; i < maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries - 1) throw; }
    }
    return default(T)!;
}
Public Async Function RetryOnFault(Of T)(func As Func(Of Task(Of T)), maxTries As Integer) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
    Next
    Return Nothing
End Function

You can then use this combinator to encode retries into the application's logic. For example:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3);

You can extend the RetryOnFault function further. For example, the function could accept another Func<Task> that the function invokes between retries to determine when to try the operation again. For example:

public static async Task<T> RetryOnFaultWithDelay<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for (int i = 0; i < maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries - 1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T)!;
}
Public Async Function RetryOnFaultWithDelay(Of T)(
    func As Func(Of Task(Of T)), maxTries As Integer, retryWhen As Func(Of Task)) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
        Await retryWhen().ConfigureAwait(False)
    Next
    Return Nothing
End Function

You can then use the function as follows to wait for a second before retrying the operation:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne

Sometimes, you can take advantage of redundancy to improve an operation's latency and chances for success. Consider multiple web services that provide stock quotes, but at various times of the day, each service might provide different levels of quality and response times. To deal with these fluctuations, you might issue requests to all the web services, and as soon as you get a response from one, cancel the remaining requests. You can implement a helper function to make it easier to implement this common pattern of launching multiple operations, waiting for any, and then canceling the rest. The NeedOnlyOne function in the following example illustrates this scenario:

public static async Task<T> NeedOnlyOne<T>(
    params Func<CancellationToken, Task<T>>[] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach (var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Stubs.Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return await completed;
}
Public Async Function NeedOnlyOne(Of T)(
    ParamArray functions As Func(Of CancellationToken, Task(Of T))()) As Task(Of T)
    Dim cts As New CancellationTokenSource()
    Dim tasks As Task(Of T)() = (From func In functions Select func(cts.Token)).ToArray()
    Dim completed As Task(Of T) = Await Task.WhenAny(tasks).ConfigureAwait(False)
    cts.Cancel()
    For Each task In tasks
        Dim ignored = task.ContinueWith(
            Sub(tsk) Stubs.Log(tsk), TaskContinuationOptions.OnlyOnFaulted)
    Next
    Return Await completed
End Function

You can then use this function as follows:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

Interleaved operations

Using the WhenAny method to support an interleaving scenario can cause a performance problem when you work with large sets of tasks. Each call to WhenAny registers a continuation with each task. For N number of tasks, this process creates O(N2) continuations over the lifetime of the interleaving operation. If you're working with a large set of tasks, use a combinator (Interleaved in the following example) to address the performance problem:

public static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception!.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}
Public Function Interleaved(Of T)(tasks As IEnumerable(Of Task(Of T))) As IEnumerable(Of Task(Of T))
    Dim inputTasks As List(Of Task(Of T)) = tasks.ToList()
    Dim sources As List(Of TaskCompletionSource(Of T)) =
        (From _i In Enumerable.Range(0, inputTasks.Count) Select New TaskCompletionSource(Of T)()).ToList()
    Dim indexRef As Integer() = {-1}
    For Each inputTask In inputTasks
        inputTask.ContinueWith(Sub(completed)
                                   Dim idx = Interlocked.Increment(indexRef(0))
                                   Dim source = sources(idx)
                                   If completed.IsFaulted Then
                                       source.TrySetException(completed.Exception.InnerExceptions)
                                   ElseIf completed.IsCanceled Then
                                       source.TrySetCanceled()
                                   Else
                                       source.TrySetResult(completed.Result)
                                   End If
                               End Sub,
                               CancellationToken.None,
                               TaskContinuationOptions.ExecuteSynchronously,
                               TaskScheduler.Default)
    Next
    Return From source In sources Select source.Task
End Function

Use the combinator to process the results of tasks as they complete. For example:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstException

In certain scatter/gather scenarios, you might want to wait for all tasks in a set, unless one of them faults. In that case, you want to stop waiting as soon as the exception occurs. You can accomplish that behavior by using a combinator method such as WhenAllOrFirstException in the following example:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception!.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => ((Task<T>)t).Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}
Public Function WhenAllOrFirstException(Of T)(tasks As IEnumerable(Of Task(Of T))) As Task(Of T())
    Dim inputs As List(Of Task(Of T)) = tasks.ToList()
    Dim ce As New CountdownEvent(inputs.Count)
    Dim tcs As New TaskCompletionSource(Of T())()

    Dim onCompleted As Action(Of Task) = Sub(completed As Task)
                                             If completed.IsFaulted Then
                                                 tcs.TrySetException(completed.Exception.InnerExceptions)
                                             End If
                                             If ce.Signal() AndAlso Not tcs.Task.IsCompleted Then
                                                 tcs.TrySetResult(inputs.Select(Function(taskItem) DirectCast(taskItem, Task(Of T)).Result).ToArray())
                                             End If
                                         End Sub

    For Each t In inputs
        t.ContinueWith(onCompleted)
    Next
    Return tcs.Task
End Function

Building task-based data structures

In addition to the ability to build custom task-based combinators, having a data structure in Task and Task<TResult> that represents both the results of an asynchronous operation and the necessary synchronization to join with it makes it a powerful type on which to build custom data structures to be used in asynchronous scenarios.

AsyncCache

One important aspect of a task is that you can hand it out to multiple consumers. All of the consumers can await it, register continuations with it, get its result or exceptions (in the case of Task<TResult>), and so on. This aspect makes Task and Task<TResult> perfectly suited to be used in an asynchronous caching infrastructure. Here's an example of a small but powerful asynchronous cache built on top of Task<TResult>:

public class AsyncCache<TKey, TValue> where TKey : notnull
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException(nameof(valueFactory));
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException(nameof(key));
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}
Public Class AsyncCache(Of TKey, TValue)
    Private ReadOnly _valueFactory As Func(Of TKey, Task(Of TValue))
    Private ReadOnly _map As New ConcurrentDictionary(Of TKey, Lazy(Of Task(Of TValue)))()

    Public Sub New(valueFactory As Func(Of TKey, Task(Of TValue)))
        If valueFactory Is Nothing Then Throw New ArgumentNullException(NameOf(valueFactory))
        _valueFactory = valueFactory
    End Sub

    Default Public ReadOnly Property Item(key As TKey) As Task(Of TValue)
        Get
            If key Is Nothing Then Throw New ArgumentNullException(NameOf(key))
            Return _map.GetOrAdd(key, Function(toAdd) New Lazy(Of Task(Of TValue))(Function() _valueFactory(toAdd))).Value
        End Get
    End Property
End Class

The AsyncCache<TKey,TValue> class accepts as a delegate to its constructor a function that takes a TKey and returns a Task<TResult>. The internal dictionary stores any previously accessed values from the cache, and the AsyncCache ensures that it generates only one task per key, even if the cache is accessed concurrently.

For example, you can build a cache for downloaded web pages:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringTaskAsync);

You can then use this cache in asynchronous methods whenever you need the contents of a web page. The AsyncCache class ensures that you're downloading as few pages as possible, and caches the results.

static AsyncCache<string, string> m_webPages =
    new AsyncCache<string, string>(url => Stubs.DownloadStringTaskAsync(url));

public static async Task UseWebPageCache(string url)
{
    string contents = await m_webPages[url];
    Console.WriteLine(contents.Length);
}
Private m_webPages As New AsyncCache(Of String, String)(Function(url) Stubs.DownloadStringTaskAsync(url))

Public Async Function UseWebPageCache(url As String) As Task
    Dim contents As String = Await m_webPages(url)
    Console.WriteLine(contents.Length)
End Function

AsyncProducerConsumerCollection

You can also use tasks to build data structures for coordinating asynchronous activities. Consider one of the classic parallel design patterns: producer/consumer. In this pattern, producers generate data that consumers consume, and the producers and consumers can run in parallel. For example, the consumer processes item 1, which was previously generated by a producer who is now producing item 2. For the producer/consumer pattern, you always need some data structure to store the work created by producers so that the consumers can be notified of new data and find it when available.

Here's a simple data structure, built on top of tasks, that enables asynchronous methods to be used as producers and consumers:

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;
            }
        }
    }
}
Public Class AsyncProducerConsumerCollection(Of T)
    Private ReadOnly m_collection As New Queue(Of T)()
    Private ReadOnly m_waiting As New Queue(Of TaskCompletionSource(Of T))()

    Public Sub Add(item As T)
        Dim tcs As TaskCompletionSource(Of T) = Nothing
        SyncLock m_collection
            If m_waiting.Count > 0 Then
                tcs = m_waiting.Dequeue()
            Else
                m_collection.Enqueue(item)
            End If
        End SyncLock
        If tcs IsNot Nothing Then tcs.TrySetResult(item)
    End Sub

    Public Function Take() As Task(Of T)
        SyncLock m_collection
            If m_collection.Count > 0 Then
                Return Task.FromResult(m_collection.Dequeue())
            Else
                Dim tcs As New TaskCompletionSource(Of T)()
                m_waiting.Enqueue(tcs)
                Return tcs.Task
            End If
        End SyncLock
    End Function
End Class

With that data structure in place, you can write code such as the following:

static AsyncProducerConsumerCollection<int> m_data = new();

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

public static void Produce(int data)
{
    m_data.Add(data);
}
Private m_data As New AsyncProducerConsumerCollection(Of Integer)()

Public Async Function ConsumerAsync() As Task
    While True
        Dim nextItem As Integer = Await m_data.Take()
        Stubs.ProcessNextItem(nextItem)
    End While
End Function

Public Sub Produce(data As Integer)
    m_data.Add(data)
End Sub

The System.Threading.Tasks.Dataflow namespace includes the BufferBlock<T> type, which you can use in a similar manner, but without having to build a custom collection type:

static BufferBlock<int> m_dataBlock = new();

public static async Task ConsumerAsyncBlock()
{
    while (true)
    {
        int nextItem = await m_dataBlock.ReceiveAsync();
        Stubs.ProcessNextItem(nextItem);
    }
}

public static void ProduceBlock(int data)
{
    m_dataBlock.Post(data);
}
Private m_dataBlock As New BufferBlock(Of Integer)()

Public Async Function ConsumerAsyncBlock() As Task
    While True
        Dim nextItem As Integer = Await m_dataBlock.ReceiveAsync()
        Stubs.ProcessNextItem(nextItem)
    End While
End Function

Public Sub ProduceBlock(data As Integer)
    m_dataBlock.Post(data)
End Sub

Note

The System.Threading.Tasks.Dataflow namespace is available as a NuGet package. To install the assembly that contains the System.Threading.Tasks.Dataflow namespace, open your project in Visual Studio, choose Manage NuGet Packages from the Project menu, and search online for the System.Threading.Tasks.Dataflow package.

See also