Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Deep guide to System.Threading.Channels for high-performance, thread-safe producer/consumer communication in .NET.
Covers channel creation, backpressure strategies, IAsyncEnumerable integration, and graceful shutdown patterns.
Scope
Channel creation (bounded and unbounded)
Backpressure strategies and capacity management
IAsyncEnumerable integration with channel readers
Graceful shutdown and drain patterns
Out of scope
Hosted service lifecycle and BackgroundService registration -- see [skill:dotnet-background-services]
Async/await fundamentals and cancellation token propagation -- see [skill:dotnet-csharp-async-patterns]
Cross-references: [skill:dotnet-background-services] for integrating channels with hosted services,
[skill:dotnet-csharp-async-patterns] for async patterns used in channel consumers.
Channel Fundamentals
A Channel<T> is a thread-safe data structure with separate ChannelWriter<T> and ChannelReader<T> endpoints.
Writers produce items, readers consume them -- the channel handles all synchronization.
// Create a channel and separate the endpoints
Channel<WorkItem> channel = Channel.CreateUnbounded<WorkItem>();
ChannelWriter<WorkItem> writer = channel.Writer;
ChannelReader<WorkItem> reader = channel.Reader;
```text
### Bounded vs Unbounded
| Aspect | Bounded | Unbounded |
| ------------- | ---------------------------------------------- | ---------------------------------- |
| Creation | `Channel.CreateBounded<T>(capacity)` | `Channel.CreateUnbounded<T>()` |
| Back-pressure | Yes -- `FullMode` controls behavior when full | No -- grows without limit |
| Memory safety | Capped at `capacity` items | Can exhaust memory under load |
| Use when | Production workloads, untrusted producer rates | Guaranteed-low-volume, prototyping |
```csharp
// Bounded -- preferred for productionvar bounded = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(capacity: 1000)
{
FullMode = BoundedChannelFullMode.Wait
});
// Unbounded -- use only when you control the producer ratevar unbounded = Channel.CreateUnbounded<WorkItem>();
```text
---
Controls what happens a bounded channel full a producer attempts to write.
| Mode | Behavior | Use |
| ------------ | ---------------------------------------------------------------- | ---------------------------------------------------- |
| `Wait` | `WriteAsync` blocks until space available | Default. Reliable delivery back-pressure |
| `DropOldest` | Drops the oldest item the channel to make room | Telemetry, metrics -- latest data matters most |
| `DropNewest` | = Channel.CreateBounded<SensorReading>( BoundedChannelOptions()
{
FullMode = BoundedChannelFullMode.DropOldest
});
logChannel = Channel.CreateBounded<LogEntry>( BoundedChannelOptions(_000)
{
FullMode = BoundedChannelFullMode.DropWrite
});
(!logChannel.Writer.TryWrite(entry))
{
overflowCounter.Add();
}
```text
Starting .NET , bounded channels drop modes accept an `itemDropped` callback that fires whenever an item
discarded. Use metrics, logging, resource cleanup dropped items.
```csharp
channel = Channel.CreateBounded( BoundedChannelOptions()
{
FullMode = BoundedChannelFullMode.DropOldest
},
itemDropped: (item, writer) =>
{
logger.LogWarning(, item.Id);
droppedItemsCounter.Add();
(item IDisposable)?.Dispose();
});
```text
The callback receives the dropped item the `ChannelWriter<T>` (useful you need to re-route items to a fallback
channel).
---
```csharp
writer.WriteAsync(item, cancellationToken);
(!writer.TryWrite(item))
{
}
```text
Multiple producers can call `WriteAsync` `TryWrite` concurrently without external locking. The channel internally
thread-safe.
```csharp
app.MapPost(, (
id,
ChannelWriter<OrderCommand> writer,
CancellationToken ct) =>
{
writer.WriteAsync( OrderCommand(id, ), ct);
Results.Accepted();
});
app.MapPost(, (
id,
ChannelWriter<OrderCommand> writer,
CancellationToken ct) =>
{
writer.WriteAsync( OrderCommand(id, ), ct);
Results.Accepted();
});
```bash
Call `Complete()` `TryComplete()` no more items will be produced. This lets consumers detect the end of the
stream.
```csharp
writer.Complete();
writer.TryComplete();
writer.TryComplete( InvalidOperationException());
```text
---
The classic pattern: wait an item, process it, repeat.
```
{
(reader.TryRead( item))
{
ProcessAsync(item, cancellationToken);
}
}
```text
This two-loop pattern preferred over `ReadAsync` alone because it drains all available items before awaiting again,
reducing state machine overhead.
For simpler cases per-item overhead acceptable:
```csharp
{
()
{
item = reader.ReadAsync(cancellationToken);
ProcessAsync(item, cancellationToken);
}
}
(ChannelClosedException)
{
}
```text
Scale processing running multiple consumer tasks. The channel ensures each item read exactly one consumer.
```
{
WorkerCount = ;
{
workers = Enumerable.Range(, WorkerCount)
.Select(i => ConsumeAsync(i, stoppingToken));
Task.WhenAll(workers);
}
{
logger.LogDebug(, workerId);
( reader.WaitToReadAsync(ct))
{
(reader.TryRead( item))
{
{
scope = scopeFactory.CreateScope();
handler = scope.ServiceProvider
.GetRequiredService<IWorkItemHandler>();
handler.HandleAsync(item, ct);
}
(Exception ex)
{
logger.LogError(ex,
,
workerId, item.Id);
}
}
}
logger.LogDebug(, workerId);
}
}
```text
---
`ChannelReader<T>.ReadAllAsync()` returns an `IAsyncEnumerable<T>`, enabling ` ` consumption integration
LINQ operators.
```
{
ProcessAsync(item, cancellationToken);
}
```text
`ReadAllAsync` the simplest consumption pattern. It handles `WaitToReadAsync`/`TryRead` internally completes
the channel closed.
Channels combine naturally ASP.NET Core streaming responses. Return the `IAsyncEnumerable<T>` directly -- minimal
APIs will stream items JSON array elements:
```csharp
app.MapGet(, (
ChannelReader<ServerEvent> reader,
CancellationToken ct) => reader.ReadAllAsync(ct));
```csharp
With the `System.Linq.Async` NuGet package, channel streams compose familiar LINQ operators:
```csharp
( batch reader.ReadAllAsync(ct)
.Where(item => item.Priority >= Priority.High)
.Buffer()
.WithCancellation(ct))
{
BulkProcessAsync(batch, ct);
}
```text
```
{
channel = Channel.CreateUnbounded<PriceUpdate>();
_ = Task.Run( () =>
{
{
( tick marketFeed.SubscribeAsync(symbol, ct))
{
channel.Writer.WriteAsync(tick, ct);
}
channel.Writer.TryComplete();
}
(Exception ex)
{
channel.Writer.TryComplete(ex);
}
}, ct);
( update channel.Reader.ReadAllAsync(ct))
{
update;
}
}
```text
---
Setting `SingleReader = ` `SingleWriter = ` channel options enables -free optimizations. The channel
trusts these hints -- = Channel.CreateBounded<T>( BoundedChannelOptions()
{
SingleReader = ,
SingleWriter = ,
FullMode = BoundedChannelFullMode.Wait
});
```text
The most efficient consumer pattern. `WaitToReadAsync` suspends until data available, then `TryRead` drains all
buffered items synchronously -- avoiding per-item state machine overhead.
```
{
(reader.TryRead( item))
{
Process(item);
}
}
```text
`TryWrite` synchronous allocation-free the channel has space. Prefer it over `WriteAsync` hot paths
you can handle the `` .
```csharp
(!writer.TryWrite(item))
{
writer.WriteAsync(item, ct);
}
```text
Bounded channels pre-allocate an array of `capacity` slots.
{
( item reader.ReadAllAsync(stoppingToken))
{
ProcessAsync(item, stoppingToken);
}
}
(OperationCanceledException) (stoppingToken.IsCancellationRequested)
{
}
```text
Complete the writer to signal no more items will arrive, then drain remaining items before stopping. This prevents data
loss during shutdown.
```
{
{
reader = channel.Reader;
{
( reader.WaitToReadAsync(stoppingToken))
{
(reader.TryRead( item))
{
scope = scopeFactory.CreateScope();
handler = scope.ServiceProvider
.GetRequiredService<IWorkItemHandler>();
handler.HandleAsync(item, stoppingToken);
}
}
}
(OperationCanceledException) (stoppingToken.IsCancellationRequested)
{
}
channel.Writer.TryComplete();
logger.LogInformation();
drainCts = CancellationTokenSource(TimeSpan.FromSeconds());
(reader.TryRead( remaining))
{
{
scope = scopeFactory.CreateScope();
handler = scope.ServiceProvider
.GetRequiredService<IWorkItemHandler>();
handler.HandleAsync(remaining, drainCts.Token);
}
(Exception ex)
{
logger.LogWarning(ex, );
}
}
logger.LogInformation();
}
}
```text
The host shutdown timeout seconds. If your drain needs more time, configure it:
```csharp
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds();
});
```text
---
**Do use unbounded channels production without rate control** -- they can exhaust memory under sustained
producer pressure. Always prefer bounded channels capacity.
**Do violate SingleReader/SingleWriter promises** -- these flags enable -free optimizations. Multiple
concurrent readers `SingleReader = ` causes data corruption, exceptions.
**Do forget to call `Complete()` the writer** -- without completion, consumers `ReadAllAsync()`
`WaitToReadAsync` will wait indefinitely after the last item.
**Do `ChannelClosedException` globally** -- it signals that the writer called `Complete()`, possibly
an error. Catch it only around `ReadAsync` calls; `WaitToReadAsync`/`TryRead` loops handle completion via ``
.
**Do use `ReadAsync` hot paths** -- prefer the `WaitToReadAsync` + `TryRead` pattern to drain buffered items
synchronously reduce state machine allocations.
**Do block the `itemDropped` callback** -- it runs synchronously the writer
## BoundedChannelFullMode
when
is
and
case
is
with
in
Drops the item being written (newest) | Rate limiting -- discard excess incoming work |
| `DropWrite` | Drops the item being written and returns `false` from `TryWrite` | Non-blocking fire-and-forget with overflow detection |
```csharp
// DropOldest -- telemetry pipeline where stale readings are expendablevar telemetryChannel
new
500
// DropWrite -- non-blocking enqueue with overflow awareness
var
new
10
if
// Channel full -- item was dropped; track overflow metric
1
### itemDropped Callback (.NET 7+)
in
7
with
is
this
for
or
on
var
new
100
"Dropped item due to channel overflow: {Id}"
1
// Clean up disposable items if needed
as
and
if
## Producer Patterns
### Single Producer
// Write with back-pressure (bounded channels)
await
// Non-blocking write attempt (returns false if channel is full or completed)
if
// Handle overflow -- log, retry, or discard
### Multiple Producers
or
is
// Multiple API endpoints enqueueing work into a shared channel
"/api/orders/{id}/process"
async
string
await
new
"process"
return
"/api/orders/{id}/cancel"
async
string
await
new
"cancel"
return
### Signaling Completion
or
when
// Signal completion -- no more items will be written
// TryComplete is idempotent -- safe to call multiple times
// Signal completion with an error
new
"Source failed"
## Consumer Patterns
### Single Consumer -- ReadAsync Loop
for
csharp
while (await reader.WaitToReadAsync(cancellationToken))
// Propagate error to reader -- ReadAllAsync will throw
await
foreach
var
in
yield
return
## Performance
### SingleReader / SingleWriter Flags
true
or
true
on
lock
violating them (multiple concurrent readers when `SingleReader = true`) causes data corruption.
```csharp
// Optimal for single-producer, single-consumer pipelinevar channel
new
1000
true
// One consumer task
true
// One producer task
### WaitToReadAsync + TryRead Pattern
is
async
csharp
while (await reader.WaitToReadAsync(ct))
// Drain all currently buffered items synchronously
while
out
var
### TryWrite Fast Path
is
and
when
in
where
false
return
// Hot path -- avoid async overhead when channel has space
if
// Slow path -- wait for space (or handle overflow)
await
### Bounded Channel Memory Behavior
internal
Items are stored byreference (for reference
types), so the channel holds references until consumed. For memory-sensitive workloads:
- Choose capacity based on expected item size multiplied by count
- Items are eligible for GC as soon as `TryRead`/`ReadAsync` returns them
- Drop modes (`DropOldest`, `DropNewest`) keep memory stable but lose data
---
## Cancellation and Graceful Shutdown
### Basic Cancellation
Pass a `CancellationToken` to all async channel operations. When cancelled, operations throw
`OperationCanceledException`.
```csharp
try