Concurrency and Multithreading:

Amit.Kumar
4 min readJun 11, 2023

Producer Consumer Problem :

The producer-consumer problem is a classic synchronization problem in concurrent programming. It involves two entities, the producer and the consumer, who share a common buffer or queue. The producer produces data items and puts them into the buffer, while the consumer consumes those data items from the buffer.

Here’s an example code that demonstrates the producer-consumer problem without synchronization:

#include <iostream>
#include <queue>
#include <thread>

std::queue<int> buffer;
const int MAX_SIZE = 10;

void producer() {
for (int i = 0; i < 20; ++i) {
if (buffer.size() < MAX_SIZE) {
buffer.push(i);
std::cout << "Produced: " << i << std::endl;
}
}
}

void consumer() {
while (true) {
if (!buffer.empty()) {
int data = buffer.front();
buffer.pop();
std::cout << "Consumed: " << data << std::endl;
}
}
}

int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);

producerThread.join();
consumerThread.join();

return 0;
}

In this code, the producer function produces data items and puts them into the buffer queue. The consumer function consumes items from the buffer. However, there is no synchronization mechanism in place, resulting in potential issues.

Without proper synchronization, race conditions and other problems may occur. For example, the consumer might try to consume data from an empty buffer, leading to undefined behavior or an infinite loop. Additionally, the producer might add items to a full buffer, overwriting existing data or causing data loss.

To fix the producer-consumer problem, we can introduce synchronization mechanisms such as mutexes and condition variables. These mechanisms ensure that the producer and consumer access the buffer safely and coordinate their actions. Here’s an updated code that resolves the issue using a mutex and condition variable:

#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

std::queue<int> buffer;
const int MAX_SIZE = 10;
std::mutex mtx;
std::condition_variable cv;

void producer() {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return buffer.size() < MAX_SIZE; });
buffer.push(i);
std::cout << "Produced: " << i << std::endl;
lock.unlock();
cv.notify_all();
}
}

void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !buffer.empty(); });
int data = buffer.front();
buffer.pop();
std::cout << "Consumed: " << data << std::endl;
lock.unlock();
cv.notify_all();
}
}

int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);

producerThread.join();
consumerThread.join();

return 0;
}

In this updated code, a mutex mtx and a condition variable cv are introduced. The mutex ensures exclusive access to the buffer, while the condition variable allows threads to wait efficiently until certain conditions are met.

The producer waits until there is space in the buffer using cv.wait() and the lambda condition function. Once there is space, the producer adds an item to the buffer, unlocks the mutex, and notifies all waiting threads using cv.notify_all().

Producer Consumer using shared and unique locks :

The std::unique_lock and std::shared_lock are two types of locks provided by the C++ Standard Library to synchronize access to shared resources. They differ in their ownership and exclusivity semantics. Here's an explanation of the differences between unique_lock and shared_lock with examples:

  1. std::unique_lock:
  • std::unique_lock provides exclusive ownership of a lock. Only one thread can hold a unique lock at a time.
  • It allows greater flexibility compared to std::lock_guard as it can be locked and unlocked multiple times during its lifetime.
  • It supports deferred locking, where the lock acquisition can be delayed until needed.
  • It supports transferring ownership of the lock between different scopes or threads.
  • It can be used with different lock types, such as std::mutex, std::recursive_mutex, or std::timed_mutex.
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void worker() {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "Thread ID: " << std::this_thread::get_id() << " acquired the lock." << std::endl;
// Critical section
std::cout << "Thread ID: " << std::this_thread::get_id() << " released the lock." << std::endl;
lock.unlock();
}

int main() {
std::thread t1(worker);
std::thread t2(worker);

t1.join();
t2.join();

return 0;
}

In this example, each thread acquires a unique lock on the mutex mtx and performs a critical section of code. The lock ensures that only one thread can execute the critical section at a time.

std::shared_lock:

  • std::shared_lock provides shared ownership of a lock. Multiple threads can hold shared locks simultaneously, allowing concurrent read-only access to the shared resource.
  • It allows multiple threads to acquire shared locks simultaneously, as long as no exclusive lock (std::unique_lock) is held by any thread.
  • It is useful when multiple threads need to read the shared resource concurrently, without modifying it.
  • It can be used with different lock types, such as std::shared_mutex or std::shared_timed_mutex.
#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_mutex mtx;
int sharedData = 0;

void reader() {
std::shared_lock<std::shared_mutex> lock(mtx);
std::cout << "Thread ID: " << std::this_thread::get_id() << " acquired a shared lock." << std::endl;
std::cout << "Shared data: " << sharedData << std::endl;
}

void writer() {
std::unique_lock<std::shared_mutex> lock(mtx);
std::cout << "Thread ID: " << std::this_thread::get_id() << " acquired an exclusive lock." << std::endl;
sharedData++; // Modify the shared data
}

int main() {
std::thread t1(reader);
std::thread t2(reader);
std::thread t3(writer);

t1.join();
t2.join();
t3.join();

return 0;
}

In this example, two threads acquire shared locks (std::shared_lock) on the shared mutex mtx and read the shared data (sharedData). Since they hold shared locks, they can concurrently access the shared resource without conflicts. The writer thread acquires an exclusive lock (std::unique_lock) on the shared mutex and modifies the shared data.

By using std::shared_lock, multiple threads can safely read the shared resource concurrently, while ensuring exclusive access for writing.

To summarize, the main difference between std::unique_lock and std::shared_lock is that std::unique_lock provides exclusive ownership, allowing only one thread to hold the lock at a time, while std::shared_lock provides shared ownership, allowing multiple threads to hold the lock simultaneously for concurrent read-only access.

--

--

Amit.Kumar

I have been a coder all my life . And yes a dreamer too. But i am also interested in understanding different aspects of life .