How to implement Producer Consumer Pattern In C#

The Producer-Consumer pattern is a popular concurrency design pattern where tasks are divided into producers and consumers. Producers are responsible for producing data, while consumers consume that data. This pattern is widely used to manage the flow of data between multiple threads, helping to avoid issues like race conditions and deadlocks.

In this blog post, we’ll explore the implementation of the Producer-Consumer pattern in C# using a simple example. We’ll create a buffer that producers will populate with data, and consumers will retrieve data from.

Let’s start with a diagram to illustrate the structure of the Producer-Consumer pattern.

Consumer
Producer
Consume Data
Buffer
Produce Data

This diagram shows the two main components: the Producer, responsible for producing data, and the Consumer, responsible for consuming data. Both interact with a shared buffer.

Code Implementation

Now, let’s dive into the code implementation in C#. We’ll use a simple example with a shared buffer, a producer class, and a consumer class.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        // Create a shared buffer with a maximum capacity of 5
        var buffer = new BlockingCollection<int>(5);

        // Create instances of producer and consumer
        var producer = new Producer(buffer);
        var consumer = new Consumer(buffer);

        // Start producer and consumer tasks
        var producerTask = Task.Run(() => producer.Produce());
        var consumerTask = Task.Run(() => consumer.Consume());

        // Wait for both tasks to complete
        Task.WaitAll(producerTask, consumerTask);
    }
}

Producer

class Producer
{
    private readonly BlockingCollection<int> buffer;

    public Producer(BlockingCollection<int> buffer)
    {
        this.buffer = buffer;
    }

    public void Produce()
    {
        for (int i = 1; i <= 10; i++)
        {
            // Produce data and add it to the buffer
            Console.WriteLine($"Producing {i}");
            buffer.Add(i);

            // Simulate some processing time
            Thread.Sleep(100);
        }

        // Mark the producer as complete
        buffer.CompleteAdding();
    }
}

Consumer

class Consumer
{
    private readonly BlockingCollection<int> buffer;

    public Consumer(BlockingCollection<int> buffer)
    {
        this.buffer = buffer;
    }

    public void Consume()
    {
        while (!buffer.IsCompleted)
        {
            try
            {
                // Consume data from the buffer
                int data = buffer.Take();
                Console.WriteLine($"Consuming {data}");

                // Simulate some processing time
                Thread.Sleep(200);
            }
            catch (InvalidOperationException)
            {
                // Handle the case when the buffer is marked as complete
            }
        }
    }
}

In this example, we use BlockingCollection as the shared buffer, which provides thread-safe blocking operations. The Producer class produces data and adds it to the buffer, while the Consumer class consumes data from the buffer. The BlockingCollection takes care of synchronization, ensuring safe communication between the producer and consumer threads.

Conclusion

The Producer-Consumer pattern is a powerful tool for managing concurrency in C# applications. By using a shared buffer and proper synchronization mechanisms, you can safely coordinate the flow of data between multiple threads. The example provided here demonstrates a simple implementation, but the principles can be applied to more complex scenarios.

Remember to handle exceptions and edge cases appropriately in real-world scenarios, and always consider the specific requirements of your application when implementing concurrency patterns.

Next Post Previous Post
No Comment
Add Comment
comment url