CancellationTokenSource
CancellationTokenSource owns the switch behind a token: create it, fire it, fire it on a delay, dispose it.
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));A CancellationToken can only report. Something has to do the asking, and that something is CancellationTokenSource, also in System.Threading. The source owns the switch. The token is a read-only view of that switch, which is why handing out a token is safe and handing out the source is not.
Creating and firing
Create a source, give cts.Token to the work, and call cts.Cancel() when it should stop. Cancel sets IsCancellationRequested to true on every copy of that token, and there is no way back. CancelAsync(), added in .NET 8, does the same and returns a Task that completes once the callbacks registered on the token have finished. Prefer it in async code, because Cancel() runs those callbacks on the thread that called it.
Firing on a delay
Two forms do the same job. new CancellationTokenSource(TimeSpan.FromSeconds(5)) arms the source as it is built; cts.CancelAfter(TimeSpan.FromSeconds(5)) arms an existing one. Both also accept a whole number of milliseconds as an int.
Ownership
Whoever creates the source cancels it, and whoever creates it disposes it. CancellationTokenSource implements IDisposable, so a using declaration is the normal way to hold one. TryReset(), added in .NET 6, disarms the timer and lets you reuse a source that was never cancelled.
Example
// The source is the switch. The token is the view of it that the work sees.
using CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Console.WriteLine($"requested before: {token.IsCancellationRequested}");
// CancelAsync was added in .NET 8. Cancel() is the synchronous form and
// runs the token's callbacks on this thread instead of returning a Task.
await cts.CancelAsync();
Console.WriteLine($"requested after: {token.IsCancellationRequested}");
try
{
// Task.Delay takes a token, so it gives up at once on a cancelled one
// instead of waiting the ten seconds out.
await Task.Delay(TimeSpan.FromSeconds(10), token);
Console.WriteLine("waited ten seconds");
}
catch (OperationCanceledException)
{
Console.WriteLine("the wait was cancelled");
}
// Output:
// requested before: False
// requested after: True
// the wait was cancelledWhen to use it
- Wiring a cancel button on a page to the work it should stop.
- Stopping a background loop cleanly when the application shuts down.
- Giving one operation a five-second budget without touching a shared client's settings.
- Cancelling the losers after the first of several parallel lookups answers.
More examples
Two ways to arm a delay
The constructor form is shorter and the CancelAfter form is more flexible, and they compile to the same behaviour, so reading one in someone else's code should not surprise you. Calling CancelAfter twice replaces the deadline rather than adding a second one, which is how you extend a deadline after making progress.
// These three sources behave identically: each cancels five seconds from now.
// Shorthand: armed by the constructor.
using CancellationTokenSource viaConstructor =
new CancellationTokenSource(TimeSpan.FromSeconds(5));
// Longhand: the same timer, set after construction. Use this form when the
// source already exists, for example a linked source you just created.
using CancellationTokenSource viaCancelAfter = new CancellationTokenSource();
viaCancelAfter.CancelAfter(TimeSpan.FromSeconds(5));
// Both the constructor and CancelAfter also take whole milliseconds as an int.
using CancellationTokenSource viaMilliseconds = new CancellationTokenSource(5000);Owning a source, handing out a token
This is the standard shape for anything with a lifetime of its own: a background loop, a long-lived connection, a job you can stop. The private field is the switch, the public property is the view, and Stop is the only way to flip it. Keeping the source private also prevents the common bug of a caller cancelling a shared source and stopping everyone else's work.
// The owner keeps the source private and exposes only the token, so no caller
// can cancel work it did not start.
sealed class PollingJob : IDisposable
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public CancellationToken Token => _cts.Token;
public void Stop()
{
_cts.Cancel();
}
public void Dispose()
{
// Dispose last. Cancel() on a disposed source throws
// ObjectDisposedException, and so does reading Token.
_cts.Dispose();
}
}Reusing a source instead of allocating one
A fresh CancellationTokenSource per operation is fine almost everywhere, and clearer. TryReset earns its place in a hot path that would otherwise allocate a source and a timer thousands of times a second. Note the false return: once the token has fired you must create a new source, so treat a false as the signal to stop.
using CancellationTokenSource cts = new CancellationTokenSource();
foreach (string url in urls)
{
cts.CancelAfter(TimeSpan.FromSeconds(30));
await FetchAsync(url, cts.Token);
// TryReset, added in .NET 6, disarms the timer and makes the same source
// usable for the next item. It returns false if cancellation has already
// been requested, because a cancelled token can never be revived.
if (!cts.TryReset())
{
break;
}
}
Discussion