Skip to Content
Course content

146: Mutexes and Condition Variables with pthreads

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was helping a colleague debug a high-frequency trading module that was intermittently crashing in production. On his local machine, it ran perfectly. In production, under heavy load, it would occasionally suffer a segmentation fault or, worse, produce corrupted trade data. After two days of staring at core dumps, we found the culprit: two threads were pushing data into the same shared circular buffer. Most of the time, they hit the buffer at different intervals, but once in a while, they tried to update the write-index at the exact same nanosecond. One thread would overwrite the other's pointer, leaving the buffer in an impossible state. It was a classic race condition, and it's exactly why we can't just "hope" that threads play nice together.

Stopping the Collision with Mutexes

When you have multiple threads accessing shared memory, you need a way to ensure that only one thread is modifying a specific piece of data at a time. This is where the pthread_mutex_t comes in. I like to think of a mutex (short for mutual exclusion) as a physical key to a room. If you want to enter the room to change a variable, you have to take the key. If someone else already has it, you have to stand outside the door and wait until they come out and hand it back.

In C, you initialize a mutex and then wrap your "critical section"—the code that touches shared data—with lock and unlock calls. If you forget to unlock, you've just created a deadlock, and your program will hang forever. I've seen senior devs do this; it happens to the best of us.

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;

void* increment(void* arg) {
    pthread_mutex_lock(&lock);
    // Critical Section: Only one thread can be here at a time
    shared_counter++; 
    pthread_mutex_unlock(&lock);
    return NULL;
}

Signaling Readiness with Condition Variables

Mutexes are great for protecting data, but they are terrible for coordination. If Thread A is waiting for Thread B to finish a task, Thread A could spend all its CPU cycles constantly locking and unlocking the mutex just to check if a flag has changed. This is called "busy-waiting," and it's a waste of resources.

Condition variables (pthread_cond_t) allow a thread to go to sleep and wake up only when a specific condition is met. The trick—and the part that trips people up—is that condition variables must always be used in tandem with a mutex. You lock the mutex, check your condition in a while loop, and then call pthread_cond_wait. This function is special: it atomically releases the mutex and puts the thread to sleep. When the thread is woken up by a signal, it automatically re-acquires the mutex before returning.

I insist on using a while loop instead of an if statement when checking the condition. This is because of "spurious wakeups," where a thread might wake up for no apparent reason. If you use if, you might proceed with the logic even though the condition is still false, leading to a crash.

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void* worker(void* arg) {
    pthread_mutex_lock(&lock);
    while (!ready) {
        pthread_cond_wait(&cond, &lock);
    }
    // Now we know 'ready' is true and we hold the lock
    printf("Worker is processing data...\n");
    pthread_mutex_unlock(&lock);
    return NULL;
}

void* signaler(void* arg) {
    pthread_mutex_lock(&lock);
    ready = 1;
    pthread_cond_signal(&cond); // Wake up one waiting thread
    pthread_mutex_unlock(&lock);
    return NULL;
}



📋 Practical Task

Exercise: Implementing a Thread-Safe Bounded Message Queue

Your task is to build a small producer-consumer system. Create a program where one "Producer" thread generates integers (1 to 10) and places them into a shared buffer of size 5, and one "Consumer" thread removes them and prints them to the console.

Requirements:

  • Use a pthread_mutex_t to protect the buffer's indices and count.
  • Use two pthread_cond_t variables: one to signal the consumer that the buffer is not empty, and one to signal the producer that the buffer is not full.
  • The producer must block (sleep) if the buffer is full.
  • The consumer must block (sleep) if the buffer is empty.
  • Ensure that the program exits cleanly after all 10 integers have been produced and consumed.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.