The System.Threading.Channels namespace contains types that you can use to implement a producer-consumer scenario, which speeds up processing by allowing producers and consumers to perform their tasks concurrently. This namespace contains a set of synchronization types that can be used for asynchronous data exchange between producers and consumers.
This article discusses how we can work with the System.Threading.Channels library in .NET Core.
Dataflow blocks vs. channels
The System.Threading.Tasks.Dataflow library encapsulates both storage and processing, and it is focused primarily on pipelining. By contrast, the System.Threading.Tasks.Channels library is focused primarily on storage. Channels are much faster than Dataflow blocks but they are specific to producer-consumer scenarios. That means they don’t support some of the control flow features that you get with Dataflow blocks.
Why use System.Threading.Channels?
You can take advantage of channels to decouple producers from consumers in a publish-and-subscribe scenario. Producers and consumers not only improve performance by working in parallel, but you can create more producers or consumers should one of those tasks begin to outstrip the other. In other words, the producer-consumer pattern helps to increase the application’s throughput (a measure that denotes the amount of work done in a unit of time).
To work with the code examples illustrated in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here.
Create a .NET Core Console App project in Visual Studio
First off, let’s create a .NET Core project in Visual Studio. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new .NET Core Console App.
- Launch the Visual Studio IDE.
- Click on “Create new project.”
- In the “Create new project” window, select “Console App (.NET Core)” from the list of the templates displayed.
- Click Next
- In the “Configure your new project” window shown next, specify the name and location for the new project.
- Click Create.
This will create a new .NET Core Console App project in Visual Studio 2019. We’ll use this project to illustrate the use of System.Threading.Channels in the subsequent sections of this article.
Install theSystem.Threading.Channels NuGet package
Now that we have created a .NET Core Console App in Visual Studio, the next thing you should do is install the necessary NuGet package. The package you need to install is System.Threading.Channels. You can install this package from the NuGet Package Manager inside the Visual Studio 2019 IDE. Alternatively, you can enter the following command to install this package via the .NET CLI.
dotnet add package System.Threading.Channels
Create a channel in .NET Core
Essentially, you can have two different types of channels. These include a bounded channel with a finite capacity and an unbounded channel with unlimited capacity. You can create either type of channel using the Channel static class that is available as part of the System.Threading.Channels namespace.
The Channel class provides the following two factory methods that can be used to create the two kinds of channels.
- CreateBounded<T> is used to create a channel that holds a finite number of messages.
- CreateUnbounded<T> is used to create a channel with unlimited capacity, i.e., a channel that (theoretically) holds an infinite number of messages.
The following code snippet illustrates how an unbounded channel can be created. Note that this channel can hold string objects only.
var channel = Channel.CreateUnbounded<string>();
Bounded channel options include a FullMode property that is used to denote the behavior of the channel during a write operation when the channel is full. Bounded channels can make use of any of the following FullMode behaviors:
- Wait
- DropWrite
- DropNewest
- DropOldest
The following code snippet illustrates how you can create a bounded channel and set the FullMode property.
Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait
});
Write data to a channel in .NET Core
To write data to a channel, you can use the WriteAsync method as shown in the code snippet given below.
await channel.Writer.WriteAsync("Hello World!");
Read data from a channel in .NET Core
To read data from a channel you can leverage the ReadAsync method as shown in the code snippet given below.
while (await reader.WaitToReadAsync())
{
if (reader.TryRead(out var message))
{
Console.WriteLine(message);
}
}
System.Threading.Channels example
Here is the 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()
{
var channel = Channel.CreateUnbounded<int>();
var reader = channel.Reader;
for(int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i + 1);
}
while (await reader.WaitToReadAsync())
{
if (reader.TryRead(out var number))
{
Console.WriteLine(number);
}
}
}
}
When executed, the above program will display the numbers 1 to 10 sequentially at the console window.
There are several ways of implementing a producer-consumer pattern, such as by using BlockingCollection or TPL Dataflow. However, channels are much faster and more efficient than either of these. I’ll discuss channels in further detail in future posts. Until then, you can learn more about the System.Threading.Channels namespace from Microsoft’s online documentation.