Producer-Consumer Problem

Thread synchronization simulation in C demonstrating semaphores and shared buffer management.

5 min read
SystemsSynchronizationMutexesTutorial

This problem is a typical synchronization problem in which two processes, a producer that generates data and a consumer that uses that data, need to share a buffer while disallowing concurrent access.

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int count = 0, in = 0, out = 0;

pthread_mutex_t lock;
pthread_cond_t not_full, not_empty;

void *producer(void *arg) {
    // TO-DO
}

void *consumer(void *arg) {
    // TO-DO
}

int main() {
    // TO-DO
}

We'll use the above structure and fill in the specific functions later on. Let's look at the code above to understand the producer-consumer problem in more detail.

We define our buffer with size 10. We also declare variables, count, in, and out. The count is going to keep track of how many items are in our buffer. The in and out will keep indices of where to produce/consume in the buffer.

We declare a lock using pthread_mutex_t. A mutex is simply a binary lock. In other words, it allows one thread to access a resource at a time. We also declare condition variables not_full and not_empty to let our other thread wait. A condition variable behaves like a queue of threads waiting for a condition to become true.

void *producer(void *arg) {
    (void) arg; // cast to void to resolve unused variable warnings
    while (1) {
        int item = rand() % 100;

        // enter critical section
        pthread_mutex_lock(&lock);

        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&not_full, &lock);
        }

        buffer[in] = item;
        in = (in + 1) % BUFFER_SIZE; // wrap around buffer
        count++;

        printf("Produced: %d\n", item);

        pthread_cond_signal(&not_empty);
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

Our producer function runs while true as we don't really have a stopping condition and want threads to access the buffer continuously. We generate a random item (let's just make it a random value between 0 and 99, but this really could be anything).

We then enter the critical section by acquiring a lock using our mutex, ensuring that the other thread can't access the shared buffer at the same time. If the buffer is full, we can't add any more produced items. In that case, we release the lock and go to sleep using pthread_cond_wait until we're signaled that space is available.

Notice that we use a while loop instead of if to check the condition. This is important because pthread_cond_wait may experience spurious wakeups or be signaled even when the condition is no longer valid. If we used if, the thread would proceed without re-checking the buffer state, potentially adding to a buffer that's already full. The while ensures that we re-check the condition each time the thread wakes up, preventing race conditions and maintaining correctness.

Once we know we can successfully add to our buffer, we do just that. At the in index of the buffer, we iterate the index by one. We can then signal to the not_empty condition variable to wake up the consumer thread. Finally, we release the lock.

void *consumer(void *arg) {
    (void) arg;
    while (1) {
        pthread_mutex_lock(&lock);

        while (count == 0) {
            pthread_cond_wait(&not_empty, &lock);
        }

        int item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;

        printf("Consumed: %d\n", item);

        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

The consumer-side is similar, but now we just consume from the buffer after the producer produced an item. It's similar to playing the snake game where apples get placed on the board and the snake eats them to get longer. Once we reach the end of the buffer, we simply wrap around and overwrite values.

int main() {
    pthread_t prod_thread, con_thread;

    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    // create two worker threads
    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&con_thread, NULL, consumer, NULL);

    // wait for threads to terminate
    pthread_join(prod_thread, NULL);
    pthread_join(con_thread, NULL);

    // cleanup
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);

    return 0;
}

In this case the producer and consumer play a game of ping pong: producing and consuming whenever a thread picks up a lock. In our main function we initialize the mutex and condition variables. We then create the two worker threads. Finally, we join so that we wait for both threads to terminate before continuing (or else the main thread might end and stop the other threads abruplty). We do some final cleanup and we're done.

The producer consumer problem is a simple synchronization problem in which we ensure no two threads have concurrent access to the buffer and make sure they write to it in their own time. The full code is shown below:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int count = 0, in = 0, out = 0;

pthread_mutex_t lock;
pthread_cond_t not_full, not_empty;

void *producer(void *arg) {
    (void) arg;
    while (1) {
        int item = rand() % 100;

        pthread_mutex_lock(&lock);

        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&not_full, &lock);
        }

        buffer[in] = item;
        in = (in + 1) % BUFFER_SIZE;
        count++;

        printf("Produced: %d\n", item);

        pthread_cond_signal(&not_empty);
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

void *consumer(void *arg) {
    (void) arg;
    while (1) {
        pthread_mutex_lock(&lock);

        while (count == 0) {
            pthread_cond_wait(&not_empty, &lock);
        }

        int item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;

        printf("Consumed: %d\n", item);

        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main() {
    pthread_t prod_thread, con_thread;

    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&con_thread, NULL, consumer, NULL);

    pthread_join(prod_thread, NULL);
    pthread_join(con_thread, NULL);

    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);

    return 0;
}

I hope you enjoyed this implementation and learned something new!

Thanks for reading! Found this useful? Share it or reach out with thoughts.

© 2025 Emir Durakovic. All rights reserved.