CancellationToken

A CancellationToken is a request to stop, not a kill switch: how to pass it down and honour it in a loop.

SyntaxcancellationToken.ThrowIfCancellationRequested();

A CancellationToken is a small value that carries one piece of news: someone has asked for this work to stop. It lives in the System.Threading namespace. You pass it into a method, and the method looks at it.

Cancellation in .NET is cooperative. The token cannot stop a thread, kill a running task, or interrupt a line of code half way through. It is a request. Work that never looks at its token runs to the end exactly as if nobody had asked.

How work honours a token

There are two ways to look. IsCancellationRequested is a bool property: check it, stop at a clean point, and keep whatever partial result you have. ThrowIfCancellationRequested() throws an OperationCanceledException when cancellation has been requested, and does nothing otherwise.

Throwing is the usual choice, because then the caller cannot mistake a cancelled call for a finished one. In a loop, check once per iteration. The gap between iterations is the only place a loop can stop cleanly.

Passing it down

By convention the token is the last parameter and is named cancellationToken. Hand it to every call you make, down to the one that actually waits: a database query, an HTTP request, Task.Delay. A token dropped half way down makes the whole chain uncancellable. When you have no token to pass, pass CancellationToken.None, a token that can never be cancelled.

Example

Example · csharp
// A token reports one thing: whether cancellation has been requested.
// The code doing the work decides when to look and what to do about it.

using CancellationTokenSource cts = new CancellationTokenSource();
List<int> collected = new List<int>();

try
{
    Collect(10, cts.Token, value =>
    {
        collected.Add(value);
        if (collected.Count == 3)
        {
            // Ask for cancellation. On its own this stops nothing.
            cts.Cancel();
        }
    });
    Console.WriteLine("collected all ten");
}
catch (OperationCanceledException)
{
    Console.WriteLine("stopped early");
}

Console.WriteLine($"values: {string.Join(", ", collected)}");

// The loop checks the token once per item. That is all "honouring" means.
static void Collect(int count, CancellationToken cancellationToken, Action<int> onValue)
{
    for (int i = 1; i <= count; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        onValue(i * i);
    }
}

// Output:
// stopped early
// values: 1, 4, 9

When to use it

  • Letting a user abandon a slow report without waiting for it to finish.
  • Stopping a long loop over database rows as soon as the caller loses interest.
  • Writing an async method other people can cancel, by taking a token and passing it on.
  • Pausing a retry loop immediately instead of sleeping out the last delay.

More examples

Stopping without throwing

IsCancellationRequested is the soft form of cancellation: the method returns normally and the caller gets a smaller number instead of an exception. Reach for this when stopping early is a legitimate outcome, such as an import that commits in batches. Reach for ThrowIfCancellationRequested instead when a partial result would be mistaken for a complete one.

Example · csharp
// Not every cancellation should throw. A job whose partial result is still
// useful can stop at a row boundary and report what it managed to do.
static class Importer
{
    public static int ImportRows(IReadOnlyList<string> rows, CancellationToken cancellationToken)
    {
        int imported = 0;
        foreach (string row in rows)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                // Stop here. The rows already counted are real.
                break;
            }

            if (row.Length > 0)
            {
                imported++;
            }
        }

        return imported;
    }
}

Carrying the token to the bottom

This is the shape almost every async method you write should have. Both HttpClient.GetAsync and HttpContent.ReadAsStringAsync have overloads that take a CancellationToken, and the second one was added in .NET 5. A chain is only as cancellable as its least cancellable link, so the review question is always: does every await on this path receive the token?

Example · csharp
static class Reports
{
    private static readonly HttpClient _client = new HttpClient();

    // The token is the last parameter, and every awaited call receives it.
    public static async Task<string> FetchAsync(string url, CancellationToken cancellationToken)
    {
        // Honour a token that was cancelled before any network work started.
        cancellationToken.ThrowIfCancellationRequested();

        using HttpResponseMessage response = await _client.GetAsync(url, cancellationToken);
        response.EnsureSuccessStatusCode();

        // Omitting the token here would make the download itself uncancellable,
        // even though the request that started it was cancellable.
        return await response.Content.ReadAsStringAsync(cancellationToken);
    }
}

Running a callback on cancellation

Register(Action) is for code that cannot take a token itself: an older API, a socket you have to close by hand, a wait that has no token-aware overload. The callback runs on the thread that calls Cancel, and if the token has already been cancelled when you register, it runs immediately on the registering thread. Keep the callback short and never let it throw, because one that throws disrupts the cancelling caller.

Example · csharp
using CancellationTokenSource cts = new CancellationTokenSource();

// Register returns a registration, which you dispose to remove the callback.
using CancellationTokenRegistration registration = cts.Token.Register(() =>
    Console.WriteLine("callback: cancellation requested"));

cts.Cancel();
// Writes: callback: cancellation requested

Discussion

  • Be the first to comment on this lesson.