Edit

Implementing the task-based asynchronous pattern

You can implement the task-based asynchronous pattern (TAP) in three ways: by using the C# and Visual Basic compilers in Visual Studio, manually, or through a combination of the compiler and manual methods. The following sections discuss each method in detail. You can use the TAP pattern to implement both compute-bound and I/O-bound asynchronous operations. The Workloads section discusses each type of operation.

Generating TAP methods

Using the compilers

Starting with .NET Framework 4.5, any method that is attributed with the async keyword (Async in Visual Basic) is considered an asynchronous method. The C# and Visual Basic compilers perform the necessary transformations to implement the method asynchronously by using TAP. An asynchronous method should return either a System.Threading.Tasks.Task or a System.Threading.Tasks.Task<TResult> object. For the latter, the body of the function should return a TResult, and the compiler ensures that this result is made available through the resulting task object. Similarly, any exceptions that go unhandled within the body of the method are marshalled to the output task and cause the resulting task to end in the TaskStatus.Faulted state. The exception to this rule is when an OperationCanceledException (or derived type) goes unhandled, in which case the resulting task ends in the TaskStatus.Canceled state.

Task.Start and task disposal

Use Start only for tasks explicitly created with a Task constructor that are still in the Created state. Public TAP methods should return active tasks, so callers shouldn't need to call Start.

In most TAP code, don't dispose tasks. A Task doesn't hold unmanaged resources in the typical case, and disposing every task adds overhead without practical benefit. Dispose only when specific APIs or measurements show a need.

If you start background work that outlives the immediate call path, keep ownership explicit and track completion. For more guidance, see Keeping async methods alive.

Generating TAP methods manually

You might implement the TAP pattern manually for better control over implementation. The compiler relies on the public surface area exposed from the System.Threading.Tasks namespace and supporting types in the System.Runtime.CompilerServices namespace. To implement the TAP yourself, you create a TaskCompletionSource<TResult> object, perform the asynchronous operation, and when it completes, call the SetResult, SetException, or SetCanceled method, or the Try version of one of these methods. When you implement a TAP method manually, you must complete the resulting task when the represented asynchronous operation completes. For example:

static class StreamExtensions
{
    public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object? state)
    {
        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;
    }
}
Module StreamExtensions
    <Extension()>
    Public Function ReadTask(stream As Stream, buffer As Byte(),
                             offset As Integer, count As Integer,
                             state As Object) As Task(Of Integer)
        Dim tcs As New TaskCompletionSource(Of Integer)()
        stream.BeginRead(buffer, offset, count,
            Sub(ar)
                Try
                    tcs.SetResult(stream.EndRead(ar))
                Catch exc As Exception
                    tcs.SetException(exc)
                End Try
            End Sub, state)
        Return tcs.Task
    End Function
End Module

Hybrid approach

You might find it useful to implement the TAP pattern manually but delegate the core logic for the implementation to the compiler. For example, you might want to use the hybrid approach when you want to verify arguments outside a compiler-generated asynchronous method so that exceptions can escape to the method's direct caller rather than being exposed through the System.Threading.Tasks.Task object:

class Calculator
{
    private int value = 0;

    public Task<int> MethodAsync(string input)
    {
        if (input == null) throw new ArgumentNullException(nameof(input));
        return MethodAsyncInternal(input);
    }

    private async Task<int> MethodAsyncInternal(string input)
    {
        // code that uses await goes here
        await Task.Delay(1);
        return value;
    }
}
Class Calculator
    Private value As Integer = 0

    Public Function MethodAsync(input As String) As Task(Of Integer)
        If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
        Return MethodAsyncInternal(input)
    End Function

    Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
        ' code that uses await goes here
        Await Task.Delay(1)
        Return value
    End Function
End Class

Another case where such delegation is useful is when you're implementing fast-path optimization and want to return a cached task.

Workloads

You can implement both compute-bound and I/O-bound asynchronous operations as TAP methods. However, when you expose TAP methods publicly from a library, provide them only for workloads that involve I/O-bound operations. These operations might also involve computation, but they shouldn't be purely computational. If a method is purely compute-bound, expose it only as a synchronous implementation. The code that consumes it can then choose whether to wrap an invocation of that synchronous method into a task to offload the work to another thread or to achieve parallelism. If a method is I/O-bound, expose it only as an asynchronous implementation.

Compute-bound tasks

The System.Threading.Tasks.Task class works well for representing computationally intensive operations. By default, it takes advantage of special support within the ThreadPool class to provide efficient execution. It also provides significant control over when, where, and how asynchronous computations execute.

Generate compute-bound tasks in the following ways:

  • In .NET Framework 4.5 and later versions (including .NET Core and .NET 5+), use the static Task.Run method as a shortcut to TaskFactory.StartNew. Use Run to easily launch a compute-bound task that targets the thread pool. This method is the preferred mechanism for launching a compute-bound task. Use StartNew directly only when you want more fine-grained control over the task.

  • In .NET Framework 4, use the TaskFactory.StartNew method. It accepts a delegate (typically an Action<T> or a Func<TResult>) to execute asynchronously. If you provide an Action<T> delegate, the method returns a System.Threading.Tasks.Task object that represents the asynchronous execution of that delegate. If you provide a Func<TResult> delegate, the method returns a System.Threading.Tasks.Task<TResult> object. Overloads of the StartNew method accept a cancellation token (CancellationToken), task creation options (TaskCreationOptions), and a task scheduler (TaskScheduler). These parameters provide fine-grained control over the scheduling and execution of the task. A factory instance that targets the current task scheduler is available as a static property (Factory) of the Task class. For example: Task.Factory.StartNew(…).

  • Use the constructors of the Task type and the Start method if you want to generate and schedule the task separately. Public methods must only return tasks that are already started.

  • Use the overloads of the Task.ContinueWith method. This method creates a new task that is scheduled when another task completes. Some of the ContinueWith overloads accept a cancellation token, continuation options, and a task scheduler for better control over the scheduling and execution of the continuation task.

  • Use the TaskFactory.ContinueWhenAll and TaskFactory.ContinueWhenAny methods. These methods create a new task that is scheduled when all or any of a supplied set of tasks completes. These methods also provide overloads to control the scheduling and execution of these tasks.

In compute-bound tasks, the system can prevent the execution of a scheduled task if it receives a cancellation request before it starts running the task. As such, if you provide a cancellation token (CancellationToken object), you can pass that token to the asynchronous code that monitors the token. You can also provide the token to one of the previously mentioned methods such as StartNew or Run so that the Task runtime can also monitor the token.

For example, consider an asynchronous method that renders an image. The body of the task can poll the cancellation token so that the code exits early if a cancellation request arrives during rendering. In addition, if the cancellation request arrives before rendering starts, you want to prevent the rendering operation:

internal static Task<Bitmap> RenderAsync(ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for (int y = 0; y < data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for (int x = 0; x < data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As CancellationToken) As Task(Of Bitmap)
    Return Task.Run(Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 To data.Height - 1
                            cancellationToken.ThrowIfCancellationRequested()
                            For x As Integer = 0 To data.Width - 1
                                ' render pixel [x,y] into bmp
                            Next
                        Next
                        Return bmp
                    End Function, cancellationToken)
End Function

Note

This sample uses Bitmap, which requires the System.Drawing.Common package and is supported only on Windows. The compute-bound task pattern—using Task.Run with a CancellationToken—applies on all platforms; substitute a cross-platform imaging library for non-Windows targets.

Compute-bound tasks end in a Canceled state if at least one of the following conditions is true:

  • A cancellation request arrives through the CancellationToken object, which is provided as an argument to the creation method (for example, StartNew or Run) before the task transitions to the Running state.

  • An OperationCanceledException exception goes unhandled within the body of such a task. That exception contains the same CancellationToken that is passed to the task, and that token shows that cancellation is requested.

If another exception goes unhandled within the body of the task, the task ends in the Faulted state. Any attempts to wait on the task or access its result causes an exception to be thrown.

I/O-bound tasks

To create a task that shouldn't directly use a thread for the entire execution, use the TaskCompletionSource<TResult> type. This type exposes a Task property that returns an associated Task<TResult> instance. You control the life cycle of this task by using TaskCompletionSource<TResult> methods such as SetResult, SetException, SetCanceled, and their TrySet variants.

Suppose you want to create a task that completes after a specified period of time. For example, you might want to delay an activity in the user interface. The System.Threading.Timer class already provides the ability to asynchronously invoke a delegate after a specified period of time. By using TaskCompletionSource<TResult>, you can put a Task<TResult> front on the timer. For example:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    TaskCompletionSource<DateTimeOffset>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(DateTimeOffset.UtcNow);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<DateTimeOffset>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
    Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(DateTimeOffset.UtcNow)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

The Task.Delay method is provided for this purpose. You can use it inside another asynchronous method, for example, to implement an asynchronous polling loop:

public static async Task Poll(Uri url, CancellationToken cancellationToken, IProgress<bool> progress)
{
    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            Await DownloadStringAsync(url)
            success = True
        Catch
            ' ignore errors
        End Try
        progress.Report(success)
    Loop
End Function

The TaskCompletionSource<TResult> class doesn't have a non-generic counterpart. However, Task<TResult> derives from Task, so you can use the generic TaskCompletionSource<TResult> object for I/O-bound methods that simply return a task. To do this, use a source with a dummy TResult (Boolean is a good default choice, but if you're concerned about the user of the Task downcasting it to a Task<TResult>, you can use a private TResult type instead). For example, the Delay method in the previous example returns the current time along with the resulting offset (Task<DateTimeOffset>). If such a result value is unnecessary, the method could instead be coded as follows (note the change of return type and the change of argument to TrySetResult):

public static Task<bool> DelaySimple(int millisecondsTimeout)
{
    TaskCompletionSource<bool>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(true);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<bool>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function DelaySimple(millisecondsTimeout As Integer) As Task(Of Boolean)
    Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(True)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of Boolean)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

Mixed compute-bound and I/O-bound tasks

Asynchronous methods aren't limited to just compute-bound or I/O-bound operations. They can represent a mixture of the two. In fact, you often combine multiple asynchronous operations into larger mixed operations. For example, the RenderAsync method in a previous example performs a computationally intensive operation to render an image based on some input imageData. This imageData could come from a web service that you asynchronously access:

public static async Task<Bitmap> DownloadDataAndRenderImageAsync(CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(cancellationToken As CancellationToken) As Task(Of Bitmap)
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

Note

This sample uses Bitmap, which requires the System.Drawing.Common package and is supported only on Windows. The pattern of chaining an async download with an async compute-bound operation applies on all platforms; substitute a cross-platform imaging library for non-Windows targets.

This example also demonstrates how a single cancellation token can be threaded through multiple asynchronous operations. For more information, see the cancellation usage section in Consuming the Task-based Asynchronous Pattern.

See also