How to use System.Threading.Channels in C#

By FoxLearn 1/3/2025 9:29:49 AM   115
The System.Threading.Channels namespace in .NET Core enables the implementation of the asynchronous producer-consumer pattern.

It provides types that facilitate efficient data exchange between producers and consumers, allowing them to operate concurrently.

Dataflow blocks vs channels

The System.Threading.Tasks.Dataflow library is designed for pipelining, combining both storage and processing, and supports complex control flow features. In contrast, the System.Threading.Tasks.Channels library focuses primarily on storage and is optimized for fast producer-consumer scenarios. Channels are faster than Dataflow blocks but lack some of the advanced control flow capabilities offered by Dataflow.

Why use System.Threading.Channels?

Channels can be used to decouple producers and consumers in a publish-and-subscribe scenario, improving performance through parallelism. The producer-consumer pattern ultimately increases the application's throughput, boosting the amount of work done in a given time period.

To get started with a .NET Core Console App in Visual Studio, you need to install the System.Threading.Channels NuGet package.

You can do this through the NuGet Package Manager in Visual Studio 2022 or by running the following command in the .NET CLI:

dotnet add package System.Threading.Channels

Create a channel in .NET Core

There are two types of channels: bounded channels with a finite capacity and unbounded channels with unlimited capacity. Both types can be created using the Channel static class in the System.Threading.Channels namespace.

The CreateBounded method creates a channel with a finite number of messages, while CreateUnbounded creates a channel with unlimited capacity, capable of holding an infinite number of messages.

For example, an unbounded channel for string objects can be created as follows:

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

Bounded channels have a FullMode property that defines the behavior when the channel is full during a write operation. This property controls how the channel behaves in such situations.

  • Wait
  • DropWrite
  • DropNewest
  • DropOldest

For example, how to create a bounded channel with a capacity of 1,000 messages and sets the FullMode property to Wait.

Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
{
    FullMode = BoundedChannelFullMode.Wait
});

This ensures that when the channel is full, the write operation will wait until space becomes available.

Write data to a channel in .NET Core

To write data to a channel, you can use the WriteAsync method, as shown in the following code:

await channel.Writer.WriteAsync(".Net Core");

This asynchronously writes the message ".Net Core" to the channel.

Read data from a channel in .NET Core

To read data from a channel, you can use the ReadAsync method along with the WaitToReadAsync method to check if there’s data to read.

while (await reader.WaitToReadAsync())
{
    if (reader.TryRead(out var message))
    {
        Console.WriteLine(message);
    }
}

For example, complete code listing that demonstrates writing and reading data to and from a channel.

class Program
{
    static async Task Main(string[] args)
    {
        await SingleProducerSingleConsumer();
        Console.ReadKey();
    }

    public static async Task SingleProducerSingleConsumer()
    {
        // Create an unbounded channel to hold string messages
        var channel = Channel.CreateUnbounded<string>();
        var reader = channel.Reader;

        // Producer: Add messages to the channel
        for (int i = 1; i <= 5; i++)
        {
            await channel.Writer.WriteAsync($"Message {i}");
        }

        // Consumer: Read messages from the channel and print them
        while (await reader.WaitToReadAsync())
        {
            if (reader.TryRead(out var message))
            {
                Console.WriteLine($"Consumed: {message}");
            }
        }
    }
}

In this example:

  • Producer: In this example, the producer writes five string messages to the channel, with each message formatted as "Message 1", "Message 2", etc.
  • Consumer: The consumer continuously checks if there are messages to read and, if available, reads each message and prints it to the console.

When executed, this program will sequentially print:

Consumed: Message 1
Consumed: Message 2
Consumed: Message 3
Consumed: Message 4
Consumed: Message 5

There are various ways to implement the producer-consumer pattern, such as using BlockingCollection or TPL Dataflow. However, channels are faster and more efficient than these alternatives.