Production services must stop work promptly on timeouts and shutdowns and use cores efficiently.
Cancellation in .NET is cooperative. A CancellationTokenSource creates a CancellationToken that you pass down through every async method. Library methods (HttpClient, EF Core, Task.Delay) observe it and throw OperationCanceledException when it fires; your own loops call token.ThrowIfCancellationRequested(). Set a timeout with new CancellationTokenSource(TimeSpan.FromSeconds(5)) or CancelAfter, and combine sources with CreateLinkedTokenSource. In ASP.NET Core, HttpContext.RequestAborted is canceled when the client disconnects.
Concurrency control:
Task.WhenAllover a thousand tasks starts them all at once. To cap concurrency, useParallel.ForEachAsync(items, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (item, ct) => ...)or aSemaphoreSlim.Task.WhenAnywaits for the first finished task, useful for timeouts or hedged requests.
For CPU-bound work, use Parallel.For/Parallel.ForEach or PLINQ (.AsParallel()), which partition work across thread pool threads. Shared state then needs synchronization: lock, Interlocked.Increment, or concurrent collections.
System.Threading.Channels provides fast async producer/consumer queues: producers WriteAsync, consumers await foreach (var x in reader.ReadAllAsync(ct)), with bounded capacity giving backpressure.
Treat OperationCanceledException as a normal outcome, not an error, when your own token was canceled.
Example
using System.Threading.Channels;
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250));
try
{
for (int i = 0; ; i++) { await Task.Delay(100, cts.Token); Console.Write($"tick{i} "); }
}
catch (OperationCanceledException) { Console.WriteLine("canceled"); }
// tick0 tick1 canceled (exact count is timing-dependent)
int done = 0;
await Parallel.ForEachAsync(Enumerable.Range(1, 8),
new ParallelOptions { MaxDegreeOfParallelism = 3 },
async (n, ct) => { await Task.Delay(20, ct); Interlocked.Increment(ref done); });
Console.WriteLine(done); // 8
var ch = Channel.CreateBounded<int>(2);
var producer = Task.Run(async () =>
{
for (int i = 0; i < 4; i++) await ch.Writer.WriteAsync(i);
ch.Writer.Complete();
});
await foreach (var x in ch.Reader.ReadAllAsync()) Console.Write(x); // 0123
await producer;Remember
- Pass CancellationToken through every async call chain
- Cap concurrency with Parallel.ForEachAsync or SemaphoreSlim
- Use Interlocked, lock or concurrent collections for shared state
- Channels give async producer/consumer queues with backpressure
Watch out for
- Accepting a CancellationToken parameter but never passing it to the calls that do the waiting.