Threading – Producer-Consumer Pattern

You can become a serverless blackbelt. Enrol to my 4-week online workshop Production-Ready Serverless and gain hands-on experience building something from scratch using serverless technologies. At the end of the workshop, you should have a broader view of the challenges you will face as your serverless architecture matures and expands. You should also have a firm grasp on when serverless is a good fit for your system as well as common pitfalls you need to avoid. Sign up now and get 15% discount with the code yanprs15!

Having run into a bit of deadlocking issue while working on Bingo.Net I spent a bit of time reading into the Producer-Consumer pattern and here’s what I learnt which I hope you’ll find useful too.

AutoResetEvent and ManualResetEvent

To start off, MSDN has an introductory article on how to synchronize a producer and a consumer thread. It’s a decent starting point, however, as some of the comments pointed out the sample code is buggy and allows for a race condition to happen when the AutoResetEvent is reset in quick succession whilst the consumer thread is processing the previous reset.

The problem with the AutoResetEvent is that you can set an event that is already set which does not have any effect:


And just like that, the Consumer has missed one of the items! However, you can work around this easily enough by locking onto the queue’s sync root object when it wakes up and dequeue as many item as there are on the queue. This might not always be applicable to your requirement though.

A much better choice would be to use the Monitor class instead.

Using Monitor.Pulse and Monitor.PulseAll

As Jon Skeet pointed out in his Threading article (see reference) the Auto/ManualResetEvent and Mutex classes are significantly slower than Monitor.

Using the Monitor.Wait, Monitor.Pulse and Monitor.PulseAll methods, you can allow multiple threads to communicate with each other similar to the way the reset events work but only safer. I have found quite a number of different implementations of the Producer-Consumer pattern using these two methods, see the references section for details.

Whilst the implementations might differ and depending on the situation sometimes you might want multi-producer/consumer support and other times you might want to ensure there can only be one producer/consumer. Whatever your requirement might be, the general idea is that both producer(s) and consumer(s) have to acquire a lock on the same sync object before they can add or remove items from the queue. And depending on who’s holding the lock:

  • producer(s) would acquire a lock, add item to the queue, and then pulse, which gives up the lock and wait up other waiting threads (the consumers)
  • consumer(s) would acquire a lock, start listening for items in a continuous loop and wait for a pulse, which gives up the lock (allowing other producers/consumers to get in and acquire the lock). When the consumer is woken up it reacquires the lock and can then safely process new items from the queue.

There are a number of other considerations you should take into account, such as the exit conditions of the consumer’s continuous loop, etc. Have a look at the different implementations I have included in the reference section to get a feel of what you need to do to implement the Producer-Consumer pattern to suit your needs.

Parting thoughts..

Another thing which you should be aware of when implementing any sort of Producer-Consumer model is the need to make the queue thread-safe seeing as multiple threads can be reading/writing to it concurrently. As you might know already, there is a Queue.Synchronized method which you can use to get a synchronized (in other words, thread-safe) version of the queue object.

But as discussed on the BCL Team Blog (see reference), the synchronized wrapper pattern gave developers a false sense of security and led some Microsoft developers to write unsafe code like this:

if (syncQueue.Count > 0) {
    Object obj = null;
    try {
        // count could be changed by this point, hence invalidating the if check
        bj = syncQueue.Dequeue();
    catch (Exception) { } // this swallows any indication we have a race condition
    // use obj here, dealing with null.

Which is why they decided it’s better to force developers to use explicit lock statement around the whole operation.

Another way to think about the problem is to picture the Queue.Synchronized method as the equivalent of a volatile modifier for a reference type which guarantees the latest value of the instance (as opposed to the reference pointer only). Which means it’s save to use in atomic operations (a single instruction) but does not stop interleaving instructions from multiple threads between operations.


MSDN article on how to synchronize a producer and a consumer thread

Jon Skeet’s article on WaitHandles

Jon Skeet’s article on Deadlocks and an implementation of the Producer-Consumer pattern using Monitor

Mike Kempf’s implementation of the Producer-Consumer pattern using Monitor

BCL Team Blog post on why there is not Queue<T>.Synchronized

StackOverflow question on why there’s no Queue<T>.Synchronized

Liked this article? Support me on Patreon and get direct help from me via a private Slack channel or 1-2-1 mentoring.
Subscribe to my newsletter

Hi, I’m Yan. I’m an AWS Serverless Hero and I help companies go faster for less by adopting serverless technologies successfully.

Are you struggling with serverless or need guidance on best practices? Do you want someone to review your architecture and help you avoid costly mistakes down the line? Whatever the case, I’m here to help.

Hire me.

Skill up your serverless game with this hands-on workshop.

My 4-week Production-Ready Serverless online workshop is back!

This course takes you through building a production-ready serverless web application from testing, deployment, security, all the way through to observability. The motivation for this course is to give you hands-on experience building something with serverless technologies while giving you a broader view of the challenges you will face as the architecture matures and expands.

We will start at the basics and give you a firm introduction to Lambda and all the relevant concepts and service features (including the latest announcements in 2020). And then gradually ramping up and cover a wide array of topics such as API security, testing strategies, CI/CD, secret management, and operational best practices for monitoring and troubleshooting.

If you enrol now you can also get 15% OFF with the promo code “yanprs15”.

Enrol now and SAVE 15%.

Check out my new podcast Real-World Serverless where I talk with engineers who are building amazing things with serverless technologies and discuss the real-world use cases and challenges they face. If you’re interested in what people are actually doing with serverless and what it’s really like to be working with serverless day-to-day, then this is the podcast for you.

Check out my new course, Learn you some Lambda best practice for great good! In this course, you will learn best practices for working with AWS Lambda in terms of performance, cost, security, scalability, resilience and observability. We will also cover latest features from re:Invent 2019 such as Provisioned Concurrency and Lambda Destinations. Enrol now and start learning!

Check out my video course, Complete Guide to AWS Step Functions. In this course, we’ll cover everything you need to know to use AWS Step Functions service effectively. There is something for everyone from beginners to more advanced users looking for design patterns and best practices. Enrol now and start learning!