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.