.NET Academy

Async & concurrency cheat sheet

Tasks, await, WhenAll, cancellation, channels and the pitfalls to avoid.

Basics

async Task<string> LoadAsync(string url, CancellationToken ct = default)
{
    using var http = new HttpClient();
    return await http.GetStringAsync(url, ct);      // waits without blocking the thread
}
 
async Task SaveAsync() { await db.SaveChangesAsync(); }        // no result β†’ Task
string html = await LoadAsync("https://example.com");          // top-level await works
Return typeUse for
TaskNo result
Task<T>A result of type T
ValueTask<T>Hot paths that usually complete synchronously
IAsyncEnumerable<T>Async streams β€” await foreach
async voidEvent handlers only

Concurrency

// Sequential β€” one after another
var a = await GetAAsync();
var b = await GetBAsync();
 
// Concurrent β€” start first, then await
Task<A> ta = GetAAsync();
Task<B> tb = GetBAsync();
await Task.WhenAll(ta, tb);
 
string[] pages = await Task.WhenAll(urls.Select(u => http.GetStringAsync(u)));
Task first = await Task.WhenAny(t1, t2);               // first to finish
await Task.Delay(500);                                 // non-blocking wait
var result = await Task.Run(() => HeavyCpuWork());     // CPU-bound work only

Cancellation and timeouts

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));   // timeout
cts.Cancel();                                                           // manual
await DoWorkAsync(cts.Token);
token.ThrowIfCancellationRequested();                                   // in loops
 
try { await op; } catch (OperationCanceledException) { /* cancelled or timed out */ }
 
using var linked = CancellationTokenSource.CreateLinkedTokenSource(requestToken);
linked.CancelAfter(TimeSpan.FromSeconds(10));
await task.WaitAsync(TimeSpan.FromSeconds(5));                          // TimeoutException

Thread safety

Interlocked.Increment(ref counter);
lock (gate) { /* short critical section β€” no await inside */ }
await semaphore.WaitAsync(); try { ... } finally { semaphore.Release(); }   // async lock
var cache = new ConcurrentDictionary<string, Product>();   cache.GetOrAdd(key, Load);

Channels (producer / consumer)

var channel = Channel.CreateBounded<Job>(100);             // back-pressure when full
await channel.Writer.WriteAsync(job);   channel.Writer.Complete();
await foreach (var j in channel.Reader.ReadAllAsync(ct)) await ProcessAsync(j);

Parallel CPU work

Parallel.ForEach(items, item => Process(item));
await Parallel.ForEachAsync(urls, new ParallelOptions { MaxDegreeOfParallelism = 4 },
    async (url, ct) => await DownloadAsync(url, ct));

Pitfalls

❌ Don'tβœ… Do
task.Result, task.Wait(), GetAwaiter().GetResult()await task β€” async all the way
async void DoWork()async Task DoWorkAsync()
Call an async method without awaiting itAwait it, or queue real background work
Task.Run(() => db.Query()) for I/OUse the async API: await db.QueryAsync()
Thread.Sleep(1000)await Task.Delay(1000)
lock around awaitSemaphoreSlim.WaitAsync()
Ignore CancellationToken parametersPass them down to every async call