C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
146: Mutexes and Condition Variables with pthreads
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_tto protect the buffer's indices and count. - Use two
pthread_cond_tvariables: 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.
There are no comments for now.