Implementing a Multi-Threaded Producer-Consumer Pattern with a Thread-Safe Ring Buffer

Semaphore Initialization

The sem_init() function accepts three parameters:

  • Parameter 1: Pointer to the semaphore variable
  • Parameter 2: Sharing mode (0 for threads within a process, non-zero for processes)
  • Parameter 3: Initial semaphore count

Semaphore vs Mutex Acquisition Order

Acquiring the semaphore before the mutex is critical for performance. This ordering allows threads to block at the semaphore level rather than contending for the lock imediately. If the mutex were acquired first, both semaphore and lock operations would execute sequentially, eliminating any concurrency benefits.

Multi-Producer Multi-Consumer vs Single-Producer Single-Consumer

Both models guarantee exclusive access—one producer pushes while one consumer pops at any given time. The multi-producer/multi-consumer variant excels at overlapping production and consumptoin workloads. Since task creation and task processing typically dominate execution time, parallelizing these phases significantly improves throughput.

Implementation

computation_task.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>

class ComputationTask
{
public:
    ComputationTask()
    {
    }

    ComputationTask(int a, int b) : _operandA(a), _operandB(b), _computedResult(0)
    {
    }

    void Execute()
    {
        _computedResult = _operandA + _operandB;
    }

    void operator()()
    {
        Execute();
    }

    std::string ToString()
    {
        return std::to_string(_operandA) + " + " + std::to_string(_operandB) + " = ?";
    }

    std::string GetResult()
    {
        return std::to_string(_operandA) + " + " + std::to_string(_operandB) 
               + " = " + std::to_string(_computedResult);
    }

private:
    int _operandA;
    int _operandB;
    int _computedResult;
};

circular_buffer.hpp

#pragma once

#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>

template <typename T>
class CircularBuffer
{
private:
    void Acquire(sem_t &semaphore)
    {
        sem_wait(&semaphore);
    }

    void Release(sem_t &semaphore)
    {
        sem_post(&semaphore);
    }

public:
    CircularBuffer(size_t capacity)
        : _storage(capacity)
        , _capacity(capacity)
        , _readPos(0)
        , _writePos(0)
    {
        sem_init(&_itemCount, 0, 0);
        sem_init(&_slotCount, 0, capacity);

        pthread_mutex_init(&_readMutex, nullptr);
        pthread_mutex_init(&_writeMutex, nullptr);
    }

    void Produce(T *element)
    {
        Acquire(_slotCount);
        pthread_mutex_lock(&_writeMutex);
        _storage[_writePos] = *element;
        _writePos = (_writePos + 1) % _capacity;
        pthread_mutex_unlock(&_writeMutex);
        Release(_itemCount);
    }

    void Consume(T *element)
    {
        Acquire(_itemCount);
        pthread_mutex_lock(&_readMutex);
        *element = _storage[_readPos];
        _readPos = (_readPos + 1) % _capacity;
        pthread_mutex_unlock(&_readMutex);
        Release(_slotCount);
    }

    ~CircularBuffer()
    {
        sem_destroy(&_itemCount);
        sem_destroy(&_slotCount);
        pthread_mutex_destroy(&_readMutex);
        pthread_mutex_destroy(&_writeMutex);
    }

private:
    std::vector<T> _storage;
    size_t _capacity;
    size_t _readPos;
    size_t _writePos;

    sem_t _itemCount;
    sem_t _slotCount;

    pthread_mutex_t _readMutex;
    pthread_mutex_t _writeMutex;
};

application.cpp

#include "circular_buffer.hpp"
#include "computation_task.hpp"
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <cstdlib>

void* ConsumerRoutine(void* args)
{
    CircularBuffer<ComputationTask>* buffer = 
        static_cast<CircularBuffer<ComputationTask>*>(args);
    
    while (true)
    {
        usleep(100000);
        ComputationTask task;
        buffer->Consume(&task);

        task();
        std::cout << "Consumed: " << task.GetResult() << std::endl;
    }
}

void* ProducerRoutine(void* args)
{
    CircularBuffer<ComputationTask>* buffer = 
        static_cast<CircularBuffer<ComputationTask>*>(args);
    
    while (true)
    {
        int x = rand() % 10 + 1;
        usleep(x * 10000);
        int y = rand() % 10 + 1;
        ComputationTask newTask(x, y);

        buffer->Produce(&newTask);
        std::cout << "Produced: " << newTask.ToString() << std::endl;
    }
}

int main()
{
    srand(time(nullptr));
    CircularBuffer<ComputationTask>* buffer = new CircularBuffer<ComputationTask>(5);
    
    pthread_t consumerThread1, consumerThread2, consumerThread3;
    pthread_t producerThread1, producerThread2, producerThread3, producerThread4;

    pthread_create(&consumerThread1, nullptr, ConsumerRoutine, buffer);
    pthread_create(&consumerThread2, nullptr, ConsumerRoutine, buffer);
    pthread_create(&consumerThread3, nullptr, ConsumerRoutine, buffer);

    pthread_create(&producerThread1, nullptr, ProducerRoutine, buffer);
    pthread_create(&producerThread2, nullptr, ProducerRoutine, buffer);
    pthread_create(&producerThread3, nullptr, ProducerRoutine, buffer);
    pthread_create(&producerThread4, nullptr, ProducerRoutine, buffer);

    pthread_join(consumerThread1, nullptr);
    pthread_join(consumerThread2, nullptr);
    pthread_join(consumerThread3, nullptr);
    pthread_join(producerThread1, nullptr);
    pthread_join(producerThread2, nullptr);
    pthread_join(producerThread3, nullptr);
    pthread_join(producerThread4, nullptr);

    delete buffer;
    return 0;
}

Tags: C++ multithreading producer-consumer Semaphore mutex

Posted on Thu, 24 Sep 2026 16:41:03 +0000 by seran128