C# Channels - Produce & Consume data

A thread-safe feature for producing and consumer data

Home DailyDrop

Daily Knowledge Drop

Today we dive into a little-known C# feature, I'd previously never heard about called Channels.

So what is a channel? - In short, a channel is a feature which allows for passing of data between a producer and consumer(s). It is an efficient, thread-safe queuing mechanism.


Usage

The examples set out below are very simple, and do not reflect a real world scenario. They have eben kept as minimal as possible to display the core concepts of the Channel. The example consists of:

  • an end point which when called will produce an item to the channel
  • a background service which will constantly monitor the channel for new items and process them

Setup

First a Channel needs to be created, and to be made available to both the producer and consumer.

This is done by declaring a singleton instance of the channel and adding it to the dependency injection container:

// Create a new channel and add to to the DI container
// CreateUnbounded => unlimited capacity
// Channel will hold Guids
builder.Services.AddSingleton<Channel<Guid>>(
    Channel.CreateUnbounded<Guid>(
            new UnboundedChannelOptions() { SingleReader = true }
    )
);

// Add as a background hosted service
// NOT related to channels directly, just used for this demo
builder.Services.AddHostedService<ChannelProcessor>();

* When creating a channel, the options UnBounded (has unlimited capacity) or *Bounded (which has limited capacity). Unbounded queues are potentially dangerous to use if the consumer is not able to keep up with the producer, resulting in the application running out of memory.

** These options are not required, but by specifying them the factory method is able to specialize the implication created to be as optimal as possible.

Producer

Here an background service is used to read items from the channel:

// The singleton implementation of the channel is injected 
// into the endpoint using dependency injection
app.MapGet("/{id}", async (Guid Id, Channel<Guid> channel) =>
{
    // The data is written to the channel
    await channel.Writer.WriteAsync(Id);
    Console.WriteLine($"Item '{Id}' successfully written to the channel");

    return await Task.FromResult($"Item '{Id}' successfully written to the channel");
});

As simple as that. Now we need a process to consume the data.


Consumer

Here an endpoint is used to write an item to the channel.

class ChannelProcessor : BackgroundService
{
    private readonly Channel<Guid> _channel;

    // The singleton implementation of the channel is injected 
    // into the background service using dependency injection
    public QueueProcessor(Channel<Guid> channel)
    {
        _channel = channel;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Items are read off the channel as they arrive and processed
        await foreach (var item in _channel.Reader.ReadAllAsync(stoppingToken))
        {
            Console.WriteLine($"Item {item} successfully read from the channel");
        }
    }
}

Output

If we run the application and hit the endpoint (to produce to the channel), we will see that it is consumed and processed immediately:

Channel demo output

We have a simple working example of data being shared efficiently and safely across threads.


Notes

This is a very simple example, and there is a lot more to Channels beyond this. Channels may not be a well-known feature, and not something which will be used in every day development - however just the knowledge they exist is a great starting point if you ever require them. They are a simple, but powerful mechanism for exchanging data across Tasks - and should definitely be leveraged if the use case arises.


References

An Introduction to System.Threading.Channels
DevBlogs: An Introduction to System.Threading.Channels

Daily Drop 08: 10-02-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net channel queue producer consumer