Multithreading in C
Learn parallel programming in C! Master pthreads (POSIX threads), C11 threads, mutex locks, condition variables, and thread synchronization.
Track Your Progress
Sign in to save your learning progress
What You Will Learn
- ✓Understand threads vs processes
- ✓Create and manage pthreads
- ✓Use mutexes for synchronization
- ✓Avoid race conditions and deadlocks
- ✓Know C11 threads.h basics
A Brief History of Multithreading
1960s - The Beginning
Timesharing systems emerged. CPUs could only run one task at a time, so the OS rapidly switched between tasks to give the illusion of parallelism.
1995 - POSIX Threads (pthreads)
IEEE standardized pthreads (IEEE 1003.1c), providing a common API for Unix-like systems. This became the de facto standard for C threading.
2005+ - Multi-Core Era
CPU clock speeds hit physical limits. Instead of faster cores, manufacturers added MORE cores. Multithreading became essential for performance.
2011 - C11 Standard
C11 added threads.h - a portable threading library. Now C has official, cross-platform thread support!
Today: Even smartphones have 8+ cores! Understanding multithreading is crucial for writing efficient, modern software.
?Why Use Multithreading?
Modern CPUs have multiple cores. Without threads, your program uses only ONE core! Multithreading lets you run code in parallel for faster execution.
Single-Threaded vs Multi-Threaded Execution
Single-Threaded
Time: 3 units (sequential)
✓Multi-Threaded
Time: 1 unit (parallel) 3x faster!
Performance
Use all CPU cores
Responsiveness
UI stays responsive
Parallelism
Divide and conquer
Two APIs: POSIX threads (pthreads) work on Linux/macOS/Unix. C11 threads.h is portable but less common.
01Threads vs Processes
Before diving into threads, let's understand the difference between a processand a thread. They're both ways to achieve parallelism, but work very differently!
Memory Layout: Process vs Thread
Two Separate Processes
Process A
Process B
Completely isolated memory
One Process, Two Threads
Process
Shared memory (except stack)
Process (Separate Houses)
- •Separate memory space (own house)
- •Heavy to create (~10,000+ CPU cycles)
- •Isolated = safer (crash doesn't affect others)
- •Need IPC (pipes, sockets) to communicate
Thread (Rooms in Same House)
- •Shared memory (same house)
- •Lightweight (~1,000 CPU cycles)
- •Shared = needs care (race conditions)
- •Direct memory sharing (fast!)
Thread Lifecycle
New
Created
Running
Executing
Blocked
Waiting for lock/IO
Terminated
Finished
Key Insight
Threads share memory, so they can easily pass data. But this also means you need synchronization to avoid race conditions! This is both threads' greatest strength and their biggest challenge.
02Creating Threads (pthreads)
Use pthread_create() to start a new thread andpthread_join() to wait for it to finish.
1#include <stdio.h>2#include <pthread.h>3#include <unistd.h>45// Thread function - must return void* and take void*6void* print_numbers(void* arg) {7 int id = *(int*)arg;8 9 for (int i = 1; i <= 5; i++) {10 printf("Thread %d: %d\n", id, i);11 sleep(1); // Sleep 1 second12 }13 14 return NULL;15}1617int main() {18 pthread_t thread1, thread2;19 int id1 = 1, id2 = 2;20 21 // Create threads22 pthread_create(&thread1, NULL, print_numbers, &id1);23 pthread_create(&thread2, NULL, print_numbers, &id2);24 25 printf("Main: Both threads started\n");26 27 // Wait for threads to finish28 pthread_join(thread1, NULL);29 pthread_join(thread2, NULL);30 31 printf("Main: Both threads finished\n");32 return 0;33}Compile with -pthread:
gcc -pthread pthread_basic.c -o pthread_basic03Passing Data to Threads
You can pass data to threads via the void* argument. Use a struct to pass multiple values!
1#include <stdio.h>2#include <pthread.h>3#include <stdlib.h>45// Struct to pass multiple arguments6typedef struct {7 int start;8 int end;9 long result;10} SumArgs;1112void* sum_range(void* arg) {13 SumArgs* args = (SumArgs*)arg;14 args->result = 0;15 16 for (int i = args->start; i <= args->end; i++) {17 args->result += i;18 }19 20 printf("Sum from %d to %d = %ld\n", 21 args->start, args->end, args->result);22 return NULL;23}2425int main() {26 pthread_t t1, t2;27 SumArgs args1 = {1, 50000000, 0};28 SumArgs args2 = {50000001, 100000000, 0};29 30 // Calculate in parallel31 pthread_create(&t1, NULL, sum_range, &args1);32 pthread_create(&t2, NULL, sum_range, &args2);33 34 pthread_join(t1, NULL);35 pthread_join(t2, NULL);36 37 long total = args1.result + args2.result;38 printf("Total sum: %ld\n", total);39 40 return 0;41}04Mutex: Preventing Race Conditions
A race condition occurs when multiple threads access shared data simultaneously. Use a mutex (mutual exclusion) to lock access!
Understanding Race Conditions
Both threads try to do counter++ which is actually 3 steps:
READ
Get value
MODIFY
Add 1
WRITE
Save value
Race Condition (counter = 0)
Result: 1 (should be 2!)
✓With Mutex (counter = 0)
Result: 2 ✓ Correct!
Bathroom Lock Analogy
Think of a mutex like a bathroom lock:
- • Only one person can use the bathroom at a time
- • You lock the door when entering (pthread_mutex_lock)
- • Others must wait outside
- • You unlock when done (pthread_mutex_unlock)
1#include <stdio.h>2#include <pthread.h>34int counter = 0;5pthread_mutex_t lock;67void* increment(void* arg) {8 for (int i = 0; i < 1000000; i++) {9 pthread_mutex_lock(&lock); // Lock10 counter++; // Critical section11 pthread_mutex_unlock(&lock); // Unlock12 }13 return NULL;14}1516int main() {17 pthread_t t1, t2;18 19 // Initialize mutex20 pthread_mutex_init(&lock, NULL);21 22 pthread_create(&t1, NULL, increment, NULL);23 pthread_create(&t2, NULL, increment, NULL);24 25 pthread_join(t1, NULL);26 pthread_join(t2, NULL);27 28 // Destroy mutex29 pthread_mutex_destroy(&lock);30 31 // Without mutex: unpredictable value < 200000032 // With mutex: always 200000033 printf("Counter: %d (expected: 2000000)\n", counter);34 35 return 0;36}Without Mutex
The counter might be less than 2,000,000 because both threads can read and write simultaneously, losing some increments. This is a race condition!
05Avoiding Deadlocks
A deadlock happens when two threads wait for each other's locks forever. It's like two people blocking each other in a narrow hallway!
Visual: How Deadlock Happens
Deadlock!
Has A
Wants B
Has B
Wants A
Both stuck forever!
✓Same Order = Safe
Same order = no conflict! ✓
The 4 Conditions for Deadlock (Coffman Conditions)
Deadlock can ONLY occur when ALL four conditions are true simultaneously. Breaking ANY one prevents deadlock:
1. Mutual Exclusion
Resource can only be held by one thread
2. Hold and Wait
Thread holds resources while waiting for more
3. No Preemption
Can't force a thread to release its lock
4. Circular Wait
T1 waits for T2, T2 waits for T1 (cycle)
Prevention Strategies
- Lock Ordering: Always acquire locks in the same order
- Try Lock: Use
pthread_mutex_trylock()to avoid blocking - Minimize: Keep critical sections as small as possible
- Avoid: Try not to hold multiple locks when possible
06Condition Variables
Condition variables let threads wait for a condition to become true, instead of spinning in a loop (wasting CPU). Perfect for producer-consumer patterns!
Producer-Consumer Pattern
Creates data
signal()
"Data ready!"
Shared Buffer
wait()
"Need data"
Uses data
Consumer wait()s until Producer signal()s that data is ready. No busy-waiting = no wasted CPU!
Busy Waiting (Bad)
// Wastes CPU cycles!
while (!ready) {
// spin spin spin...
}
CPU runs at 100% doing nothing useful
✓Condition Wait (Good)
// Thread sleeps efficiently
while (!ready) {
pthread_cond_wait(&cond, &lock);
}
Thread sleeps until woken up
1#include <stdio.h>2#include <pthread.h>3#include <stdbool.h>45int data = 0;6bool ready = false;7pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;8pthread_cond_t cond = PTHREAD_COND_INITIALIZER;910void* producer(void* arg) {11 pthread_mutex_lock(&lock);12 13 data = 42;14 ready = true;15 printf("Producer: Data is ready!\n");16 17 pthread_cond_signal(&cond); // Wake up consumer18 pthread_mutex_unlock(&lock);19 20 return NULL;21}2223void* consumer(void* arg) {24 pthread_mutex_lock(&lock);25 26 while (!ready) { // Wait for data27 printf("Consumer: Waiting...\n");28 pthread_cond_wait(&cond, &lock);29 }30 31 printf("Consumer: Got data = %d\n", data);32 pthread_mutex_unlock(&lock);33 34 return NULL;35}3637int main() {38 pthread_t prod, cons;39 40 pthread_create(&cons, NULL, consumer, NULL);41 sleep(1); // Let consumer start waiting42 pthread_create(&prod, NULL, producer, NULL);43 44 pthread_join(prod, NULL);45 pthread_join(cons, NULL);46 47 return 0;48}07C11 threads.h (Portable Alternative)
Why Did C11 Add threads.h?
Before C11
- • No standard threading in C language
- • Linux/macOS: pthreads
- • Windows: Win32 threads
- • Code not portable between systems!
✓After C11
- • Official
<threads.h>in C standard - • Same API works everywhere
- • Write once, compile anywhere
- • Simpler API than pthreads
Note: Compiler support varies. GCC 12+ and Clang 17+ support it. MSVC has limited support. For maximum compatibility, pthreads is still widely used.
C11 threads.h Components
thrd_t
Thread handle
mtx_t
Mutex
cnd_t
Condition variable
tss_t
Thread-local storage
Thread Functions
| Function | Description | Returns |
|---|---|---|
| thrd_create(&t, func, arg) | Create new thread | thrd_success / thrd_error |
| thrd_join(t, &result) | Wait for thread to finish | thrd_success / thrd_error |
| thrd_detach(t) | Detach thread (auto-cleanup) | thrd_success / thrd_error |
| thrd_current() | Get current thread ID | thrd_t |
| thrd_sleep(&dur, &rem) | Sleep for duration | 0 / -1 (interrupted) |
| thrd_yield() | Yield to other threads | void |
| thrd_exit(result) | Exit current thread | Does not return |
Mutex Functions
Mutex Types
mtx_plain- Basic mutexmtx_timed- Supports timeoutmtx_recursive- Same thread can lock multiple timesFunctions
mtx_init(&mtx, type)
mtx_lock(&mtx)
mtx_trylock(&mtx)
mtx_timedlock(&mtx, &ts)
mtx_unlock(&mtx)
mtx_destroy(&mtx)
Condition Variable Functions
cnd_init(&cnd);
// Wait for condition (releases mutex while waiting)
cnd_wait(&cnd, &mtx);
// Wait with timeout
cnd_timedwait(&cnd, &mtx, &ts);
// Wake one waiting thread
cnd_signal(&cnd);
// Wake all waiting threads
cnd_broadcast(&cnd);
cnd_destroy(&cnd);
Thread-Local Storage (TLS)
Each thread gets its own copy of a variable. Perfect for per-thread data like error codes!
Using _Thread_local keyword
// Each thread has its own copy
_Thread_local int error_code = 0;
// C23 also allows:
thread_local int error_code = 0;
Using tss_t (dynamic)
tss_t key;
tss_create(&key, destructor);
tss_set(key, value);
tss_get(key);
tss_delete(key);
Complete Example with C11 threads.h
1#include <stdio.h>2#include <threads.h>3#include <stdatomic.h>45// Shared data6int counter = 0;7mtx_t lock;8cnd_t done_cond;9int threads_done = 0;1011// Thread function - must return int (not void*)12int increment(void* arg) {13 int id = *(int*)arg;14 15 for (int i = 0; i < 1000000; i++) {16 mtx_lock(&lock);17 counter++;18 mtx_unlock(&lock);19 }20 21 // Signal that this thread is done22 mtx_lock(&lock);23 threads_done++;24 printf("Thread %d finished. Total done: %d\n", id, threads_done);25 cnd_signal(&done_cond);26 mtx_unlock(&lock);27 28 return 0; // Return value (can be retrieved via thrd_join)29}3031int main() {32 thrd_t t1, t2;33 int id1 = 1, id2 = 2;34 35 // Initialize mutex and condition variable36 if (mtx_init(&lock, mtx_plain) != thrd_success) {37 printf("Failed to create mutex\n");38 return 1;39 }40 if (cnd_init(&done_cond) != thrd_success) {41 printf("Failed to create condition variable\n");42 return 1;43 }44 45 // Create threads46 if (thrd_create(&t1, increment, &id1) != thrd_success) {47 printf("Failed to create thread 1\n");48 return 1;49 }50 if (thrd_create(&t2, increment, &id2) != thrd_success) {51 printf("Failed to create thread 2\n");52 return 1;53 }54 55 // Wait for both threads using condition variable56 mtx_lock(&lock);57 while (threads_done < 2) {58 cnd_wait(&done_cond, &lock);59 }60 mtx_unlock(&lock);61 62 // Or simply join (wait for completion)63 int result1, result2;64 thrd_join(t1, &result1);65 thrd_join(t2, &result2);66 67 printf("Thread 1 returned: %d\n", result1);68 printf("Thread 2 returned: %d\n", result2);69 printf("Final counter: %d (expected: 2000000)\n", counter);70 71 // Cleanup72 mtx_destroy(&lock);73 cnd_destroy(&done_cond);74 75 return 0;76}Compile:
gcc -std=c11 -pthread c11_threads_complete.c -o threads_demoNote: Some implementations still need -pthread for C11 threads
🆚 Complete pthreads vs C11 threads.h Comparison
| Feature | pthreads | C11 threads.h |
|---|---|---|
| Thread type | pthread_t | thrd_t |
| Create | pthread_create() | thrd_create() |
| Join | pthread_join() | thrd_join() |
| Mutex type | pthread_mutex_t | mtx_t |
| Condition var | pthread_cond_t | cnd_t |
| Thread-local | pthread_key_t | tss_t / _Thread_local |
| Function return | void* | int |
| Portability | Unix/Linux/macOS | Cross-platform (C11+) |
| Compiler support | Excellent | Growing |
Use pthreads when:
- • Targeting Unix/Linux/macOS only
- • Need advanced features (barriers, spin locks)
- • Maximum compiler compatibility
- • Existing codebase uses pthreads
Use C11 threads.h when:
- • Need cross-platform portability
- • Starting a new project
- • Want simpler, cleaner API
- • Using modern C11+ compiler
Pro Tip: Check Compiler Support
Use __STDC_NO_THREADS__ to check if threads.h is available:
#ifdef __STDC_NO_THREADS__
#error "C11 threads not supported!"
#endif
08Common Threading Patterns
Worker Pool
Fixed number of threads that process tasks from a queue. Limits thread creation overhead.
Producer-Consumer
One thread creates data, another consumes it. Uses condition variables for signaling.
Read-Write Lock
Multiple readers OR one writer. Use pthread_rwlock_t for this pattern.
!Code Pitfalls: Common Mistakes & What to Watch For
Common Mistakes with Multithreading
Copying code frequently generate multithreaded code with critical concurrency bugs:
- ✗Missing mutex locks: Beginners often forget to protect shared data, creating race conditions that only appear under load
- ✗Deadlock-prone patterns: Beginners often lock mutexes in inconsistent order across different functions
- ✗Missing pthread_join: Beginners often create threads but forgets to join them, causing premature termination or zombie threads
- ✗Non-volatile flags: Code often uses regular variables for inter-thread communication without volatile sig_atomic_t
Always Understand Before Using
Multithreading bugs are notoriously hard to reproduce — code may work 99 times and fail once. Use tools like ThreadSanitizer (-fsanitize=thread) and Helgrind to detect race conditions. Copied code can't reason about timing — you must.
10Frequently Asked Questions
Q:What's the difference between threads and processes?
A: Threads share memory within a process — lighter to create and switch. Processes have separate memory — more isolation but higher overhead. Use threads for parallelism within one program; processes for running separate programs or when isolation is critical.
Q:What is a race condition?
A: When multiple threads access shared data and at least one writes, the result depends on timing. Thread A reads x=5, Thread B writes x=10, Thread A writes x=6 — final value is unpredictable! Fix with mutexes to ensure only one thread accesses data at a time.
Q:What is a deadlock and how do I prevent it?
A: When two threads each hold a lock the other needs, both wait forever. Prevention: always acquire locks in the same order, use timeouts with pthread_mutex_trylock(), or design to minimize lock holding time.
Q:Why must I call pthread_join()?
A: Without join, the main thread might exit before worker threads finish, killing them mid-work. Join waits for thread completion and cleans up resources. Alternatively, usepthread_detach() for threads that don't need joining.
Q:How many threads should I create?
A: For CPU-bound work, use number of CPU cores (check with sysconf(_SC_NPROCESSORS_ONLN)). For I/O-bound work, more threads can help since they'll be waiting anyway. Too many threads = overhead from context switching. Profile to find the sweet spot.
10Summary
Complete Threading Workflow
pthread_create()
Start thread
mutex_lock()
Protect data
// work
Do the task
mutex_unlock()
Release lock
pthread_join()
Wait for finish
Key Functions
pthread_create— Start a threadpthread_join— Wait for threadpthread_mutex_*— Prevent racespthread_cond_*— Wait for events
✓Best Practices
- Always protect shared data with mutex
- Lock in consistent order (no deadlocks)
- Keep critical sections short
- Compile with
-pthreadflag
Quick Reference: Common Pitfalls
Race Condition
Fix: Use mutex to protect shared data
Deadlock
Fix: Always lock in same order
Memory Leak
Fix: Always join or detach threads
Test Your Knowledge
Related Tutorials
Signals and Signal Handling
Handle UNIX/Linux signals in C! Learn about SIGINT, SIGTERM, SIGSEGV, signal handlers, sigaction, and writing signal-safe code.
C23 Standard Features
Explore the newest C standard! Learn about typeof, constexpr, nullptr, _BitInt, bool/true/false as keywords, [[attributes]], and other C23 improvements.
Dynamic Memory Allocation
Allocate memory at runtime! Use malloc, calloc, realloc, and free. Create arrays whose size you don't know until the program runs.
Have Feedback?
Found something missing or have ideas to improve this tutorial? Let us know on GitHub!