Implementing producer-consumer patterns with `std::jthread`

In concurrent programming, the producer-consumer pattern is commonly used to handle communication between two threads - one thread produces data and the other thread consumes the data.

With the introduction of C++20, the new std::jthread class provides a convenient way to manage threads and their lifetimes. Let’s see how we can use std::jthread to implement the producer-consumer pattern.

Producer

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

std::queue<int> dataQueue;
std::mutex mtx;
std::condition_variable cv;

void producer()
{
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dist(1, 100);

    for (int i = 0; i < 10; ++i) {
        int data = dist(gen);

        std::lock_guard<std::mutex> lock(mtx);
        dataQueue.push(data);
        std::cout << "Produced: " << data << std::endl;

        cv.notify_one(); // Notify consumer
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}

In the producer function, we generate random data using std::random_device and std::mt19937. We then push the data into the dataQueue using a mutex for synchronization. We also notify the consumer thread using the cv.notify_one() method after producing each item.

Consumer

void consumer()
{
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !dataQueue.empty(); });

        int data = dataQueue.front();
        dataQueue.pop();
        std::cout << "Consumed: " << data << std::endl;

        lock.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

In the consumer function, we create a loop that waits for the dataQueue to not be empty. We use a std::unique_lock along with the cv.wait() method to wait until data is available for consumption. Once data is available, we retrieve it from the queue, print it out, and then sleep for one second before consuming the next item.

Putting it all together

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

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

    return 0;
}

In the main() function, we create two std::jthread objects - one for the producer and one for the consumer. We pass the respective functions producer and consumer as arguments to the thread constructors. After starting the threads, we wait for them to finish by calling join() on each of the threads. Finally, we return 0 to indicate successful termination.

By using the std::jthread class along with mutexes and condition variables, we can easily implement the producer-consumer pattern in C++. This allows for safe communication and synchronization between producer and consumer threads.

#C++ #Multithreading