Producer-Consumer Problem Using Mutex in C++

Domi Yan
Level Up Coding
Published in
4 min readDec 16, 2020

--

The producer-consumer problem is a classical multi-threaded synchronization problem in concurrent programming. Today, we will try to address it in C++ with mutexes.

This tutorial consists of three parts. First, we define and analyze the challenging part of the quiz. In the second section, we explain and learn to use mutexes in C++. Finally, we apply mutexes to accomplish our solution.

Problem Statement

The producer-consumer problem is a set of problems with lots of variants. In this tutorial, we focus on the simplest version:

  1. The system has one producer thread and one consumer thread.
  2. The size of the buffer between producer and consumer is exactly one. In other words, the producer will wait for the consumer to consume the product before producing the next one.

The critical point is the producer and the consumer both need to access the shared resource (buffer) without knowing what the other is doing. We must handle the synchronization properly so that no data provided by the producer is lost (producer generates the next product before consumer accept) or duplicated (producer fails to update before consumer consume obtain next one). To achieve the goal, the desired solution should guarantee:

  1. The access of data by two threads are mutually exclusive. When one is accessing the data, the other must be blocked.
  2. The activity of producer and consumer are alternating in a “ping-pong” pattern. In an efficient implementation, the consumer launches immediately after the producer completes, and vice versa.

Before we can tackle the problem, we need to study how to use mutexes.

Mutex

In multi-threaded programming, sharing resources across threads must be dealt with caution as data-race can occur. We need a mechanism to guarantee some visit to shared resources is mutually exclusive from others. Mutex is designed for this purpose. The following exhibits the common usage of mutexes in C++.

std::mutex (from header<mutex>)is declared as a global variable and referenced across different threads. Each thread can trigger lock() and unlock() to mark the start and end of a critical section.

A critical section includes operations that need to be protected and avoid concurrent accesses. In the example of the producer-consumer, the code to produce (write to the shared resource) and consume (read from shared resource) must be guarded.

In modern C++ (since C++11), an enhancement was made to mutexes.

Use Mutexes with std::unique_lock

std::unique_lock is an RAII wrapper for std::mutex. It gets the benefits of RAII: automatically locks the mutex in construction and unlocks it when gets destructed, provides exception safety. Developers can also manually unlock/lock it. Here is a rewritten version of the above example with std::unique_lock:

If you don’t understand RAII, don’t worry, it does not affect this tutorial. Just remember for now that using std::unique_lock with std::mutex is an improvement over raw mutexes. For the rest of this article, we will follow this practice.

We have the hammer now, now let’s work out the solution!

Producer-Consumer Solution with Mutex

This code snippet demonstrates the core logic of the solution: (It excludes parts readers don’t have to focus on: the main function, launching threads, etc.)

The system contains a consumer function and a producer function that each executes an infinite loop to keep generating/accepting data.

At the top, three global variables are declared to facilitate communication between threads:

  1. g_mutex is the mutex variable (line1).
  2. g_ready is a flag to notify the other thread “I am done my part” (line 2).
  3. g_data is the one-size buffer for storing data (line 3).

During execution, the producer performs:

  1. Lock the critical section (line 20).
  2. Produce data (line 22).
  3. Set the flag g_ready to true (line 23).
  4. Unlock the critical section (line 24), expecting the consumer to take it and change the flag.
  5. Keep waiting, until g_ready to be false (line 25).

and consumer acts:

  1. Wait for g_ready to be true (line 8) indicating the producer finishes its job.
  2. Lock the critical section (line 11) (Automatically unlocked at line 14 when goes out of scope).
  3. Consume the data. (line 12).
  4. Set flag g_ready to false(line 13) notifying its work is done.

Noticed that we have to introduce a while loop on both sides to wait for the other thread. This strategy is called “busy-waiting”. Busy-waiting is inefficient because it costs the processor time doing useless activities and should be avoided. We can add a sleep command in each loop to reduce the frequency of checking global status g_ready.

Here is the complete version of the code with “sleeping wait”:

This program will switch between the producer and consumer every second.

You can probably identify this is still not perfect because there is no way we know how long in the general case does it take to generate/use the data and a thread should “sleep”. Setting a random waiting time is not optimal. We will address this issue in this tutorial.

Summary

In this article, we learned:

  1. The challenge of the producer-consumer problem: synchronization/coordination between threads.
  2. Use std::mutex and std::unique_lock in C++ to protect critical section in threads.
  3. Solve the producer-consumer problem with mutexes.

Reference

  1. Producer–consumer problem
  2. std::mutex — cppreference.com
  3. std::unique_lock — cppreference.com
  4. RAII

--

--