0Pricing
C# Academy · Lesson

System.Threading.Channels

Build producer-consumer pipelines using Channel, ChannelWriter, and ChannelReader for backpressure control.

System.Threading.Channels is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Are Channels?

System.Threading.Channels (introduced in .NET Core 3) provides a high-performance, thread-safe producer-consumer queue. Unlike BlockingCollection, Channels are fully async and allocation-efficient — ideal for in-process pipelines.

Creating a Channel

Channels are created with a factory. Choose unbounded (no limit) or bounded (limited capacity with backpressure). The factory returns a Channel<T> with a Writer and a Reader.

using System.Threading.Channels;

// Unbounded: unlimited capacity, no backpressure
var unbounded = Channel.CreateUnbounded<string>();

// Bounded: max 100 items; writer waits when full
var bounded = Channel.CreateBounded<string>(100);

// Bounded with drop-oldest strategy:
var dropping = Channel.CreateBounded<string>(new BoundedChannelOptions(50)
{
    FullMode = BoundedChannelFullMode.DropOldest
});

Writing to a Channel

Producers write items using WriteAsync (waits if bounded channel is full) or TryWrite (returns false immediately if full). Signal completion with Writer.Complete().

var channel = Channel.CreateUnbounded<int>();

// Producer task
var producer = Task.Run(async () =>
{
    for (int i = 0; i < 100; i++)
    {
        await channel.Writer.WriteAsync(i);
        await Task.Delay(10);
    }
    channel.Writer.Complete(); // signal no more items
});

Reading from a Channel

Consumers read with ReadAllAsync() (the simplest approach) or ReadAsync/TryRead for more control. ReadAllAsync completes when the writer calls Complete().

// Consumer task
var consumer = Task.Run(async () =>
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        Console.WriteLine($"Processed: {item}");
        // Naturally paced — waits for next item
    }
    Console.WriteLine("Channel completed");
});

await Task.WhenAll(producer, consumer);

Multiple Producers

Channels are thread-safe. Multiple producers can write concurrently — no locking required. Call Writer.Complete() only after all producers finish.

var channel = Channel.CreateUnbounded<WorkItem>();

var producers = Enumerable.Range(0, 4).Select(id =>
    Task.Run(async () =>
    {
        for (int i = 0; i < 25; i++)
            await channel.Writer.WriteAsync(new WorkItem(id, i));
    }));

// Wait for all producers before completing the writer
await Task.WhenAll(producers);
channel.Writer.Complete();

Multiple Consumers (Fan-Out)

Run multiple consumer tasks on the same channel reader to parallelise processing. Each item is delivered to exactly one consumer (partitioned, not broadcast).

var channel = Channel.CreateBounded<WorkItem>(100);

// 4 parallel consumers
var consumers = Enumerable.Range(0, 4).Select(id =>
    Task.Run(async () =>
    {
        await foreach (var item in channel.Reader.ReadAllAsync())
        {
            await ProcessItemAsync(item);
            Console.WriteLine($"Consumer {id} processed {item.Id}");
        }
    }));

await Task.WhenAll(consumers);

Pipeline Pattern

Chain channels together into a processing pipeline: each stage reads from one channel, processes items, and writes to the next. Stages run concurrently with natural backpressure.

// Stage 1: raw data
var stage1 = Channel.CreateBounded<string>(50);
// Stage 2: parsed
var stage2 = Channel.CreateBounded<ParsedRecord>(50);
// Stage 3: enriched
var stage3 = Channel.CreateBounded<EnrichedRecord>(50);

var parse   = ParseStageAsync(stage1.Reader, stage2.Writer);
var enrich  = EnrichStageAsync(stage2.Reader, stage3.Writer);
var persist = PersistStageAsync(stage3.Reader);

await Task.WhenAll(parse, enrich, persist);

Backpressure with Bounded Channels

A bounded channel automatically applies backpressure: when the channel is full, WriteAsync suspends the producer until the consumer drains items. No manual throttling code needed.

// Bounded channel: max 10 items
var channel = Channel.CreateBounded<string>(10);

// Fast producer
var producer = Task.Run(async () =>
{
    for (int i = 0; i < 1000; i++)
    {
        // WriteAsync waits when channel has 10 items
        await channel.Writer.WriteAsync($"item-{i}");
        // Producer is naturally slowed to consumer speed
    }
    channel.Writer.Complete();
});

// Slow consumer
var consumer = Task.Run(async () =>
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        await Task.Delay(50); // simulate slow processing
        Console.WriteLine(item);
    }
});

Error Handling in Channels

Pass an exception to Writer.Complete(exception) to propagate errors to all waiting readers. Readers see the exception when the channel is next read.

var channel = Channel.CreateUnbounded<int>();

var producer = Task.Run(async () =>
{
    try
    {
        for (int i = 0; i < 100; i++)
        {
            if (i == 50) throw new Exception("Producer failed at 50");
            await channel.Writer.WriteAsync(i);
        }
        channel.Writer.Complete();
    }
    catch (Exception ex)
    {
        channel.Writer.Complete(ex); // propagate to reader
    }
});

try
{
    await foreach (var item in channel.Reader.ReadAllAsync())
        Console.WriteLine(item);
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Channel error: {ex.Message}");
}

Real-World: Background Job Queue

A background job queue using a hosted service and a channel: HTTP requests enqueue jobs; a background worker processes them one at a time.

public class JobQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue
        = Channel.CreateBounded<Func<CancellationToken, Task>>(100);

    public ChannelWriter<Func<CancellationToken, Task>> Writer => _queue.Writer;
    public ChannelReader<Func<CancellationToken, Task>> Reader => _queue.Reader;
}

public class JobProcessor : BackgroundService
{
    private readonly JobQueue _queue;
    public JobProcessor(JobQueue q) => _queue = q;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await foreach (var job in _queue.Reader.ReadAllAsync(ct))
            await job(ct);
    }
}

Quick Check

What happens when a producer calls WriteAsync on a full bounded channel?

Recap: System.Threading.Channels

Key takeaways:

  • Channels provide a high-performance, async-native producer-consumer queue
  • Unbounded: no limit; Bounded: finite capacity with backpressure or drop strategies
  • Multiple producers and consumers are safe without locking
  • ReadAllAsync() + await foreach = cleanest consumption pattern
  • Chain channels into pipelines for stage-based concurrent processing
  • Call Writer.Complete() (or Complete(exception)) to signal end-of-stream

Frequently asked questions

Is the “System.Threading.Channels” lesson free?

Yes — the full text of “System.Threading.Channels” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “System.Threading.Channels”?

Build producer-consumer pipelines using Channel, ChannelWriter, and ChannelReader for backpressure control. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “System.Threading.Channels” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. IAsyncEnumerable & await foreach
  2. System.Threading.Channels
  3. ValueTask & Avoiding Allocations
  4. ConfigureAwait & Synchronization Context
← Back to C# Academy