C Multithreading — POSIX Threads (pthreads) for Concurrent Programming
In this tutorial, you will learn about C Multithreading. We cover key concepts, practical examples, and best practices to help you master this topic.
C multithreading using POSIX threads (pthreads) enables concurrent execution with thread creation (pthread_create), synchronization (mutexes, condition variables), and thread management (join, detach) for parallel processing on multi-core systems.
What You Will Learn
- Creating threads with pthread_create and joining with pthread_join
- Protecting shared data with mutexes (pthread_mutex_t)
- Signaling between threads with condition variables
- Thread-local storage with __thread
- Avoiding deadlocks and race conditions
- Thread safety of standard library functions
Why It Matters
Modern CPUs have multiple cores that sit idle if your program is single-threaded. Multithreading lets you Process data in parallel, handle multiple clients simultaneously, and keep the UI responsive while doing background work. POSIX threads are the standard threading API on Unix-like systems (Linux, macOS, BSD). Durga Antivirus Pro uses a thread pool to scan multiple files concurrently, with one thread per CPU core, reducing full-system scan time from hours to minutes.
Real-World Use
A web server creates a new thread for each incoming connection. While one thread serves a slow client downloading a large file, another thread handles a quick API request from a different client. Without threads, the fast request would wait for the slow download to complete.
Learning Path
flowchart LR A[Signals] --> B[Multithreading\nYou are here] B --> C[Network Sockets] style B fill:#f90,color:#fff
Creating and Joining Threads
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void* print_numbers(void *arg) {
int id = *(int*)arg;
for (int i = 1; i <= 5; i++) {
printf("Thread %d: %d\n", id, i);
usleep(100000); // 100ms
}
return NULL;
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
// Create two threads
if (pthread_create(&t1, NULL, print_numbers, &id1) != 0) {
perror("Failed to create thread 1");
return 1;
}
if (pthread_create(&t2, NULL, print_numbers, &id2) != 0) {
perror("Failed to create thread 2");
return 1;
}
// Wait for both threads to complete
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Both threads completed.\n");
return 0;
}
Compile with -pthread:
gcc -pthread -o threads threads.c
./threads
Output (interleaved, may vary):
Thread 1: 1
Thread 2: 1
Thread 1: 2
Thread 2: 2
Thread 1: 3
Thread 2: 3
...
Returning Values from Threads
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct {
int start;
int end;
int result;
} Range;
void* sum_range(void *arg) {
Range *r = (Range*)arg;
r->result = 0;
for (int i = r->start; i <= r->end; i++) {
r->result += i;
}
return NULL;
}
int main() {
Range r1 = {1, 50000000, 0};
Range r2 = {50000001, 100000000, 0};
pthread_t t1, t2;
pthread_create(&t1, NULL, sum_range, &r1);
pthread_create(&t2, NULL, sum_range, &r2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
long long total = (long long)r1.result + r2.result;
printf("Sum 1..100000000 = %lld\n", total);
return 0;
}
Mutex Synchronization
A mutex prevents multiple threads from accessing shared data simultaneously:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* increment(void *arg) {
int id = *(int*)arg;
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
shared_counter++;
pthread_mutex_unlock(&mutex);
}
printf("Thread %d done\n", id);
return NULL;
}
int main() {
pthread_t threads[5];
int ids[5];
for (int i = 0; i < 5; i++) {
ids[i] = i + 1;
pthread_create(&threads[i], NULL, increment, &ids[i]);
}
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
printf("Final counter: %d (expected: 500000)\n", shared_counter);
pthread_mutex_destroy(&mutex);
return 0;
}
Without the mutex, the counter would be less than 500,000 due to race conditions.
Condition Variables
Condition variables let threads wait for a specific condition:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
int data = 0;
void* producer(void *arg) {
for (int i = 1; i <= 5; i++) {
sleep(1);
pthread_mutex_lock(&mutex);
data = i * 100;
ready = 1;
printf("Produced: %d\n", data);
pthread_cond_signal(&cond); // Wake up one consumer
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void *arg) {
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex);
// Wait while condition is false (spurious wakeup safe)
while (!ready) {
pthread_cond_wait(&cond, &mutex);
}
printf("Consumed: %d\n", data);
ready = 0;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
Thread-Local Storage
Each thread gets its own copy of a variable declared with __thread:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// Each thread has its own copy of this variable
__thread int thread_local_counter = 0;
void* worker(void *arg) {
int id = *(int*)arg;
thread_local_counter = id * 100;
printf("Thread %d: thread_local_counter = %d\n", id, thread_local_counter);
// Each thread modifies only its own copy
thread_local_counter += 50;
printf("Thread %d: after increment = %d\n", id, thread_local_counter);
return NULL;
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
pthread_create(&t1, NULL, worker, &id1);
pthread_create(&t2, NULL, worker, &id2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
Thread Pool Pattern
A simple thread pool for parallel task execution:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 4
#define NUM_TASKS 20
pthread_mutex_t task_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t task_cond = PTHREAD_COND_INITIALIZER;
int next_task = 0;
int completed_tasks = 0;
void* thread_worker(void *arg) {
int id = *(int*)arg;
while (1) {
pthread_mutex_lock(&task_mutex);
// Wait if no tasks, but check if all done
while (next_task >= NUM_TASKS && completed_tasks < NUM_TASKS) {
pthread_cond_wait(&task_cond, &task_mutex);
}
if (completed_tasks >= NUM_TASKS) {
pthread_mutex_unlock(&task_mutex);
break;
}
int task = next_task++;
pthread_mutex_unlock(&task_mutex);
// Process task
printf("Thread %d processing task %d\n", id, task);
usleep(100000 + rand() % 200000);
pthread_mutex_lock(&task_mutex);
completed_tasks++;
if (completed_tasks == NUM_TASKS) {
pthread_cond_broadcast(&task_cond); // Wake all waiting threads
}
pthread_mutex_unlock(&task_mutex);
}
printf("Thread %d exiting\n", id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int ids[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, thread_worker, &ids[i]);
}
// Wake up threads to start processing
pthread_cond_broadcast(&task_cond);
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("All %d tasks completed.\n", completed_tasks);
return 0;
}
Common Mistakes
Race conditions from missing mutexes: Two threads reading/writing the same variable without synchronization causes unpredictable results. Always protect shared mutable state with a mutex.
Deadlock from multiple mutexes: If thread A locks mutex1 then mutex2, and thread B locks mutex2 then mutex1, they deadlock. Always lock mutexes in the same order.
Forgetting to unlock on error paths: If a function locks a mutex and returns early on error without unlocking, the mutex is never released. Use a goto cleanup pattern or restructure the code.
Spurious wakeups from condition variables: pthread_cond_wait can return even if the condition is not signaled. Always check the condition in a while loop, not if.
Calling pthread_join on a detached thread: Detached threads cannot be joined. Their resources are automatically reclaimed when they exit. Joining a detached thread returns an error.
Not compiling with -pthread: The pthreads library requires linking with
-pthread. Without it, you get undefined reference errors.Assuming thread-safe library functions: Most C library functions are not thread-safe by default. Functions like strtok, asctime, and rand use static internal state. Use their _r variants (strtok_r, rand_r) in threaded code.
Practice Questions
- What is a Race Condition and how does a mutex prevent it?
- Why must
pthread_cond_waitbe used inside a while loop instead of an if statement? - What is a deadlock and how can it be avoided?
- How does thread-local storage differ from a global variable protected by a mutex?
- Challenge: Implement a parallel merge sort using pthreads. Split the array in half, sort each half in a separate thread, then merge the results. Add a depth limit so threads are not created for very small sub-arrays (use sequential sort below a threshold).
Mini Project
Build a parallel file search tool:
- The program takes a directory path and a filename pattern
- It recursively scans the directory tree, collecting file paths
- It distributes the file paths across a thread pool (one thread per CPU core)
- Each thread checks if its assigned files match the pattern (using fnmatch or strstr)
- Matching files are added to a shared result list protected by a mutex
- The main thread prints results as they come in (but no faster than one per line)
- Measure the speedup compared to a single-threaded version
- Handle errors: permission denied, symlink loops, invalid path
FAQ
What is Next
Proceed to Network Sockets to learn about TCP/IP socket programming. Then explore I/O Multiplexing for handling multiple connections with select/poll/epoll.