Introduction
In today’s article, we will look at the producer-consumer pattern in C# and how we can implement this pattern using the System Threading Channels data structure. We will look at the advantages of using this data structure and see an example of it in action.
The Producer-Consumer Pattern
The Producer-Consumer pattern is where a producer generates some messages or data, as we may call it, and various consumers can read that data and work on it. The main advantage of this pattern is that the producer and consumer are not causally linked in any way.
Hence, we can say this is a disconnected pattern. The System Threading Channels data structure can be used to achieve this pattern.
It is easy to use and is thread-safe, as I will demonstrate in the below example.
Example. Let us create a console application in Visual Studio 2019 (Community edition) as below.



We then add the below code.
Example
using System;
using System.Collections.Generic;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace ConsoleAppProducerConsumerPattern
{
class Program
{
static void Main(string[] args)
{
var pc = new ProducerConsumer();
pc.StartChannel();
Console.ReadKey();
}
}
public class ProducerConsumer
{
static int messageLimit = 5;
Channel<string> channel = Channel.CreateBounded<string>(messageLimit);
public void StartChannel()
{
List<string> names = new List<string>
{
"John Smith",
"Jane Smith",
"John Doe",
"Jane Doe"
};
Task producer = Task.Factory.StartNew(() =>
{
foreach (var name in names)
{
channel.Writer.TryWrite(name);
}
channel.Writer.Complete();
});
Task[] consumers = new Task[2];
for (int i = 0; i < consumers.Length; i++)
{
consumers[i] = Task.Factory.StartNew(async () =>
{
while (await channel.Reader.WaitToReadAsync())
{
if (channel.Reader.TryRead(out var data))
{
Console.WriteLine($"Data read from Consumer No.{Task.CurrentId} is {data}");
}
}
});
}
producer.Wait();
Task.WaitAll(consumers);
}
}
}
In the above example, we create a channel. There are two types of channels, bound and un-bound. Here we are using a bound channel which gives us more control over the channel, like setting the maximum number of messages that the channel can carry. This is important in a scenario where we do not want to overload the channel with producer messages to the point that they cannot be handled by the consumers.
First, we create a simple generic list of strings. These will be the messages that will be sent on the channel by the producer to be read by the consumers. Next, we create a task for the producer, which reads each string from the list and writes it to the channel. Next, we create two tasks for the consumers, which will read the messages (strings) from the channel and write them to the console. This is a simple example for demonstration purposes only. In a real-life scenario, we would probably pass some concrete object from the producer, and in the consumer, we would process that object.
If we run the application, we get the below.

Here, you can see that three items (strings) were processed by one of the consumer tasks and the remaining one by the other consumer task.
Summary
In this article, we looked at what the producer-consumer pattern is in general. Then we saw how we can implement this pattern using the System Threading Channels data structure. We looked at an example of implementing the producer-consumer pattern using a bound channel and the results we would get. Happy Coding.

karthik parchaPosted Feb 6, 2021, 10:12 AM
Same pattern I need to implement in my project.Let say producer is from .net api and consumer is console app, how to notify consumer console app.
Yury FedorovPosted Nov 22, 2020, 10:06 PM
Interesting. Do you know why Microsoft decided to do apparently third yet different implementation of consumer-producer pattern in .NET after: BufferBlock: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-implement-a-producer-consumer-dataflow-pattern BlockingCollection: https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1?view=net-5.0 What are the use cases for each of them in your opinion?
Munib ButtPosted Nov 1, 2020, 10:50 AM
This pattern is very useful for building processes that work without human intervention. This can include batch jobs triggered via some scheduled job or by some service running in the background. This pattern can help to process data that is either residing in a database or being read from some other source. The producer or producers can read this data and place it in a channel, and we can have consumer tasks that will read it from the channel and process this data. The major advantage of this pattern in parallel programming is that it is thread-safe and gives some very predictable results.