The Channel<T> model
Reader/Writer halves, bounded vs unbounded, full-mode behavior โ the vocabulary of backpressure.
The Channel<T> model
Channel<T> splits into two halves with deliberately different
capabilities: ChannelWriter<T> can write and complete; ChannelReader<T>
can read, count, and observe completion. The split is the design: consumers
cannot sneak writes, producers cannot complete the channel out from under
a reader's contract.
var ch = Channel.CreateBounded<int>(capacity: 100);
// or Channel.CreateUnbounded<int>();
await ch.Writer.WriteAsync(item, ct); // async write; WAITS when full
if (ch.Reader.TryRead(out var v)) { } // sync poll
await ch.Reader.ReadAsync(ct); // async read; WAITS when empty
await foreach (var v in ch.Reader.ReadAllAsync(ct)) { } // drain to completion
Two options shape everything else:
- Bounded vs unbounded. An unbounded channel accepts every write
instantly โ memory grows without limit under producer>consumer load. A
bounded channel makes
WriteAsyncWAIT when full. That wait is backpressure, and it is a feature, not a bug. - Full modes (
BoundedChannelFullMode):Wait(default โ writers wait),DropOldest/DropNewest(ring-buffer semantics, lose data but never stall),DropWrite. Telemetry pipelines love DropOldest; order processing must never use it.
SingleReader/SingleWriter options let the implementation skip
synchronization โ a measurable speedup for the single-pump case.
Channels exist because Queue<T> + lock + SemaphoreSlim hand-rolled
poorly: no async waiting, no completion, no backpressure. Task collections
have no flow control at all. Channel is the standard library's answer to
"go-like pipelines in C#".