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
145: Working with pthreads (POSIX Threads)
Up until now, we've written programs that follow a linear path. One instruction leads to the next, and the CPU handles them in a predictable sequence. But when you start working with pthreads, you're essentially telling the OS to run multiple instructions simultaneously. It sounds like an easy win for performance, but it's where things get dangerous. I've seen senior devs spend two days chasing a bug that only happens once every thousand runs because they treated shared memory like it was a safe haven.
Let's look at a common scenario: you're building a network packet analyzer. You want to count how many packets have been processed across several different threads to keep the UI updated in real-time. Your first instinct might be to just use a global counter.
#include <pthread.h>
#include <stdio.h>
int packet_count = 0;
void* process_packets(void* arg) {
for (int i = 0; i < 1000000; i++) {
packet_count++; // The naive approach
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, process_packets, NULL);
pthread_create(&t2, NULL, process_packets, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Total packets: %d\n", packet_count);
return 0;
}
The Illusion of the Atomic Increment
If you run the code above, you'll expect the output to be 2,000,000. Sometimes it will be. But more often, you'll get something like 1,423,842. You didn't make a logic error in your loop, so why is the number wrong? The problem is that packet_count++ isn't a single operation. To the CPU, it's actually three: load the value from memory into a register, increment the register, and store the value back into memory.
Imagine Thread A loads the value 10. Before it can store the result back, the OS switches context to Thread B. Thread B also loads 10, increments it to 11, and stores it. Then the OS switches back to Thread A, which still has 10 in its register. It increments it to 11 and stores it. Two increments happened, but the counter only went up by one. This is a race condition, and it's the bane of concurrent programming.
Using Mutexes to Enforce Order
To fix this, we need a way to ensure that only one thread is touching that counter at a time. This is where the pthread_mutex_t (mutual exclusion) comes in. Think of a mutex as a physical key to a room; if you have the key, you're the only one allowed inside. Everyone else has to wait in the hallway until you come out and hand over the key.
#include <pthread.h>
#include <stdio.h>
int packet_count = 0;
pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
void* process_packets(void* arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&count_mutex);
packet_count++;
pthread_mutex_unlock(&count_mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, process_packets, NULL);
pthread_create(&t2, NULL, process_packets, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Total packets: %d\n", packet_count);
pthread_mutex_destroy(&count_mutex);
return 0;
}
Paying the Synchronization Tax
Now the count is always 2,000,000. We've solved the data corruption, but we've introduced a performance cost. Locking and unlocking a mutex takes time. Even worse, if your threads are spending all their time waiting for the lock, you've essentially turned your multi-threaded program back into a single-threaded one, but with extra overhead. I call this "contention."
The trick to writing high-performance C is to keep your "critical sections"—the code between the lock and unlock—as short as possible. In the example above, we're locking just for a single increment. If you had to perform a heavy calculation, you'd do that calculation in a local variable first, and then lock the mutex only when it's time to add that result to the global total. Always ask yourself: "Do I actually need to lock this, or can I give each thread its own local counter and sum them up at the end?"
📋 Practical Task
Implementing a Thread-Safe Transaction Ledger
You are tasked with creating a small system that simulates a bank ledger. Multiple threads will be depositing and withdrawing money from a single shared account balance simultaneously.
Requirements:
- Create a global variable
double account_balanceinitialized to 1000.00. - Initialize a
pthread_mutex_tto protect the balance. - Write a function
void* perform_transactions(void* arg)that takes an integer (via the arg) representing the number of transactions to perform. - Inside the function, use a loop to randomly either add or subtract a small amount (between 1.00 and 10.00) from the balance.
- Ensure that the balance never drops below 0.00 (if a withdrawal would make it negative, the thread should skip that transaction).
- Spawn 4 threads, each performing 100,000 transactions.
- Join the threads and print the final balance.
Goal: Prove that your mutex is working by comparing the final result against a version where you commented out the lock/unlock calls. You should see that without the mutex, the balance drifts wildly or becomes inconsistent.
There are no comments for now.