Part 3 · 1 chapters · ~8 min
async/await and Concurrency
Tasks and the thread pool, async/await state machines, measured concurrent waits, ConfigureAwait and synchronisation contexts, sync-over-async deadlocks and thread-pool starvation, CancellationToken, Task.WhenAll with limits, Parallel.ForEachAsync, Channels for producer-consumer queues, locks and Interlocked, and ValueTask.
4
Async all the way down
code
var tasks = new Task[100_000];
for (int i = 0; i < tasks.Length; i++) tasks[i] = Task.Delay(1000);
await Task.WhenAll(tasks); // measured: 1,087 ms then 1,072 ms
// bounded fan-out with cancellation
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(2));
await Parallel.ForEachAsync(accountIds, new ParallelOptions { MaxDegreeOfParallelism = 20, CancellationToken = cts.Token },
async (id, token) => balances[id] = await ledger.GetBalanceAsync(id, token));
// producer-consumer with System.Threading.Channels
var ch = Channel.CreateBounded<PayoutJob>(capacity: 1_000); // backpressure when full
_ = Task.Run(async () => { await foreach (var job in ch.Reader.ReadAllAsync(ct)) await SubmitAsync(job, ct); });
// never: var b = ledger.GetBalanceAsync(id).Result; blocks a pool thread; starves under loadASYNC/AWAIT, MEASURED
.NET 10.0.12, Apple M3 Pro (12 cores)
swipe the figure sideways, or tap expand for full screen
1/4
concurrent waiting
100,000 tasks each waiting one second completed together in about 1.07 to 1.09 seconds: awaiting releases the thread instead of blocking it.
100k waits in ~1.07 sthreads released while waiting