-
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
140: Practice Exercise: Building a Thread-Safe Counter with C11 Threads
I've seen a lot of developers fall into the same trap when they first move from single-threaded logic to concurrent programming. It usually happens with something as simple as a counter. You think, "It's just one integer; how hard can it be to increment it from a few different threads?" The problem is that counter++ looks like a single operation in C, but the CPU sees something entirely different.
The Trap of the Simple Increment
If you just declare a global integer and let ten different threads hammer away at it in a loop, you'll notice something frustrating: the final total is almost never what you expected. You might expect 100,000, but you get 74,302. This happens because counter++ is actually three distinct steps: the CPU loads the value from memory into a register, increments that register, and then stores the value back into memory.
// The naive, broken way
int shared_counter = 0;
int increment_task(void* arg) {
for (int i = 0; i < 10000; i++) {
shared_counter++; // Race condition lives here
}
return 0;
}
When two threads do this simultaneously, they can both read the value 10 at the same time. They both increment it to 11 in their own private registers, and then they both write 11 back to memory. You just performed two increments, but the counter only went up by one. I call this "lost updates," and it's the classic symptom of a race condition.
Paying the Locking Tax for Correctness
To fix this using C11 threads, we need to ensure that only one thread can touch that integer at a time. We use mtx_t from <threads.h> to create a mutual exclusion zone. By wrapping the increment in a lock and an unlock call, we force the threads to line up and take turns.
// The thread-safe way
mtx_t counter_lock;
int shared_counter = 0;
int increment_task(void* arg) {
for (int i = 0; i < 10000; i++) {
mtx_lock(&counter_lock);
shared_counter++;
mtx_unlock(&counter_lock);
}
return 0;
}
Now the result is guaranteed to be correct. But here is the trade-off I want you to consider: performance. Locking is expensive. Every time you call mtx_lock, the thread might have to pause or communicate with the OS kernel. If you spend more time managing the lock than actually doing work, your multi-threaded program might actually run slower than a single-threaded one. I've seen people over-lock their entire main loop and wonder why their "high-performance" server is crawling.
When the Lock is Too Heavy
If you're just incrementing a number, a full mutex is often overkill. In a real-world production environment, I'd tell you to look at <stdatomic.h>. Atomic operations use specialized CPU instructions (like "Compare-and-Swap") to update the value in one indivisible step without needing a formal lock. It's significantly faster because it avoids the overhead of the mutex machinery. However, mutexes are more flexible; they can protect complex structures like linked lists or arrays where a single atomic operation isn't enough to keep the data consistent. Knowing when to use a mutex versus an atomic is what separates a junior dev from a senior engineer in the C world.
📋 Practical Task
Exercise: Implementing a Concurrent Request Tracker
You are building a mock network server. The server uses multiple threads to handle incoming requests, and it needs to keep an accurate count of the total number of requests processed across all threads to report in a final log.
Your Goal: Write a program that spawns 5 threads. Each thread should simulate processing 20,000 requests by incrementing a shared global counter. You must use mtx_t from <threads.h> to ensure that no updates are lost.
Requirements:
- Initialize a
mtx_tobject usingmtx_init. - Implement a thread function that increments the shared counter in a loop.
- Ensure the mutex is locked before the increment and unlocked immediately after.
- Join all threads back to the main thread before printing the final count.
- Clean up the mutex using
mtx_destroy.
Expected Result: The final printed value of the counter must be exactly 100,000 regardless of how the threads are scheduled.
There are no comments for now.