Introduction to POSIX Threads (pthreads)
POSIX threads (pthreads) is a standardized C language threading API for Unix-like operating systems. It provides a powerful interface for creating and managing multiple threads of execution within a single process, enabling parallel processing and improved performance on multi-core systems.
Threading Architecture Overview
Multithreading Architecture ├── Thread Management │ ├── Creation (pthread_create) │ ├── Termination (pthread_exit) │ ├── Joining (pthread_join) │ └── Detaching (pthread_detach) ├── Synchronization │ ├── Mutexes (pthread_mutex) │ ├── Condition Variables │ ├── Read-Write Locks │ ├── Barriers │ └── Semaphores └── Thread-local Storage ├── Thread-specific data └── Thread-local variables
Thread States
Created ↓ Runnable ←→ Running ↓ ↓ Blocked Terminated
Basic Thread Operations
1. Creating and Joining Threads
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// Thread function
void* thread_function(void* arg) {
int thread_num = *(int*)arg;
for (int i = 0; i < 5; i++) {
printf("Thread %d: iteration %d\n", thread_num, i);
sleep(1); // Simulate work
}
printf("Thread %d finished\n", thread_num);
return NULL;
}
int main() {
pthread_t thread1, thread2;
int t1_arg = 1, t2_arg = 2;
printf("Main: Creating threads\n");
// Create threads
if (pthread_create(&thread1, NULL, thread_function, &t1_arg) != 0) {
perror("Failed to create thread 1");
return 1;
}
if (pthread_create(&thread2, NULL, thread_function, &t2_arg) != 0) {
perror("Failed to create thread 2");
return 1;
}
printf("Main: Waiting for threads to finish\n");
// Wait for threads to complete
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Main: All threads finished\n");
return 0;
}
Compilation:
gcc -pthread threads.c -o threads ./threads
2. Thread with Return Values
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
typedef struct {
int start;
int end;
int result;
} ThreadData;
void* sum_range(void* arg) {
ThreadData* data = (ThreadData*)arg;
data->result = 0;
printf("Thread computing sum from %d to %d\n", data->start, data->end);
for (int i = data->start; i <= data->end; i++) {
data->result += i;
usleep(1000); // Small delay to simulate work
}
printf("Thread finished: sum = %d\n", data->result);
return (void*)&data->result;
}
int main() {
pthread_t thread1, thread2;
ThreadData data1 = {1, 50, 0};
ThreadData data2 = {51, 100, 0};
// Create threads
pthread_create(&thread1, NULL, sum_range, &data1);
pthread_create(&thread2, NULL, sum_range, &data2);
// Wait for threads and get results
int* result1;
int* result2;
pthread_join(thread1, (void**)&result1);
pthread_join(thread2, (void**)&result2);
int total = *result1 + *result2;
printf("\nMain: Partial sums: %d + %d = %d\n",
*result1, *result2, total);
printf("Main: Expected sum 1-100 = 5050\n");
return 0;
}
3. Thread Arguments and Context
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
typedef struct {
char name[50];
int id;
int iterations;
int delay_ms;
} ThreadContext;
void* worker_thread(void* arg) {
ThreadContext* ctx = (ThreadContext*)arg;
for (int i = 0; i < ctx->iterations; i++) {
printf("[%s-%d] Working: iteration %d\n",
ctx->name, ctx->id, i + 1);
usleep(ctx->delay_ms * 1000);
}
printf("[%s-%d] Finished\n", ctx->name, ctx->id);
// Create result string (must persist after function returns)
char* result = malloc(100);
sprintf(result, "%s-%d completed %d iterations",
ctx->name, ctx->id, ctx->iterations);
return result;
}
int main() {
pthread_t threads[3];
ThreadContext contexts[3];
// Initialize thread contexts
strcpy(contexts[0].name, "Worker");
contexts[0].id = 1;
contexts[0].iterations = 3;
contexts[0].delay_ms = 500;
strcpy(contexts[1].name, "Helper");
contexts[1].id = 2;
contexts[1].iterations = 5;
contexts[1].delay_ms = 300;
strcpy(contexts[2].name, "Daemon");
contexts[2].id = 3;
contexts[2].iterations = 2;
contexts[2].delay_ms = 800;
printf("Main: Creating threads\n");
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, worker_thread, &contexts[i]);
}
printf("Main: Waiting for threads\n");
// Collect results
for (int i = 0; i < 3; i++) {
char* result;
pthread_join(threads[i], (void**)&result);
printf("Thread %d result: %s\n", i + 1, result);
free(result);
}
printf("Main: All done\n");
return 0;
}
Thread Synchronization
1. Mutex Basics
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 5
#define NUM_ITERATIONS 100000
// Shared resource
long long counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// Thread function with mutex protection
void* increment_with_mutex(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < NUM_ITERATIONS; i++) {
pthread_mutex_lock(&mutex);
counter++; // Critical section
pthread_mutex_unlock(&mutex);
}
printf("Thread %d finished\n", thread_id);
return NULL;
}
// Thread function without mutex (race condition demo)
void* increment_without_mutex(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < NUM_ITERATIONS; i++) {
counter++; // RACE CONDITION!
}
printf("Thread %d finished (unsafe)\n", thread_id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
printf("=== Mutex Protection Demo ===\n\n");
// Test with mutex
counter = 0;
printf("With mutex protection:\n");
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, increment_with_mutex, &thread_ids[i]);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Final counter: %lld (expected: %d)\n\n",
counter, NUM_THREADS * NUM_ITERATIONS);
// Test without mutex
counter = 0;
printf("Without mutex protection (race condition):\n");
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, increment_without_mutex, &thread_ids[i]);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Final counter: %lld (expected: %d)\n",
counter, NUM_THREADS * NUM_ITERATIONS);
pthread_mutex_destroy(&mutex);
return 0;
}
2. Mutex with Error Handling
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
typedef struct {
pthread_mutex_t mutex;
int* data;
int size;
int count;
} SafeArray;
SafeArray* create_safe_array(int size) {
SafeArray* arr = malloc(sizeof(SafeArray));
if (!arr) return NULL;
// Initialize mutex with attributes
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
if (pthread_mutex_init(&arr->mutex, &attr) != 0) {
free(arr);
return NULL;
}
arr->data = calloc(size, sizeof(int));
arr->size = size;
arr->count = 0;
pthread_mutexattr_destroy(&attr);
return arr;
}
int safe_array_add(SafeArray* arr, int value) {
int ret = pthread_mutex_lock(&arr->mutex);
if (ret != 0) {
errno = ret;
return -1;
}
if (arr->count >= arr->size) {
pthread_mutex_unlock(&arr->mutex);
return -1; // Array full
}
arr->data[arr->count++] = value;
pthread_mutex_unlock(&arr->mutex);
return 0;
}
int safe_array_get(SafeArray* arr, int index, int* value) {
if (index < 0 || index >= arr->size) {
return -1;
}
pthread_mutex_lock(&arr->mutex);
if (index >= arr->count) {
pthread_mutex_unlock(&arr->mutex);
return -1; // Element not yet added
}
*value = arr->data[index];
pthread_mutex_unlock(&arr->mutex);
return 0;
}
void safe_array_destroy(SafeArray* arr) {
if (arr) {
pthread_mutex_destroy(&arr->mutex);
free(arr->data);
free(arr);
}
}
void* producer_thread(void* arg) {
SafeArray* arr = (SafeArray*)arg;
for (int i = 0; i < 10; i++) {
if (safe_array_add(arr, i * 10) == 0) {
printf("Producer added: %d\n", i * 10);
} else {
printf("Producer failed to add %d (array full)\n", i * 10);
}
usleep(100000); // 100ms delay
}
return NULL;
}
void* consumer_thread(void* arg) {
SafeArray* arr = (SafeArray*)arg;
int value;
for (int i = 0; i < 10; i++) {
if (safe_array_get(arr, i, &value) == 0) {
printf("Consumer got: %d\n", value);
} else {
printf("Consumer failed to get element %d\n", i);
}
usleep(150000); // 150ms delay
}
return NULL;
}
int main() {
SafeArray* arr = create_safe_array(10);
if (!arr) {
printf("Failed to create safe array\n");
return 1;
}
pthread_t producer, consumer;
pthread_create(&producer, NULL, producer_thread, arr);
pthread_create(&consumer, NULL, consumer_thread, arr);
pthread_join(producer, NULL);
pthread_join(consumer, NULL);
safe_array_destroy(arr);
return 0;
}
3. Condition Variables
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
typedef struct {
int buffer[BUFFER_SIZE];
int count;
int in;
int out;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
} BoundedBuffer;
void buffer_init(BoundedBuffer* buf) {
buf->count = 0;
buf->in = 0;
buf->out = 0;
pthread_mutex_init(&buf->mutex, NULL);
pthread_cond_init(&buf->not_full, NULL);
pthread_cond_init(&buf->not_empty, NULL);
}
void buffer_destroy(BoundedBuffer* buf) {
pthread_mutex_destroy(&buf->mutex);
pthread_cond_destroy(&buf->not_full);
pthread_cond_destroy(&buf->not_empty);
}
void buffer_put(BoundedBuffer* buf, int item) {
pthread_mutex_lock(&buf->mutex);
// Wait while buffer is full
while (buf->count == BUFFER_SIZE) {
printf("Buffer full, producer waiting...\n");
pthread_cond_wait(&buf->not_full, &buf->mutex);
}
// Add item to buffer
buf->buffer[buf->in] = item;
buf->in = (buf->in + 1) % BUFFER_SIZE;
buf->count++;
printf("Produced: %d (count: %d)\n", item, buf->count);
// Signal that buffer is not empty
pthread_cond_signal(&buf->not_empty);
pthread_mutex_unlock(&buf->mutex);
}
int buffer_get(BoundedBuffer* buf) {
pthread_mutex_lock(&buf->mutex);
// Wait while buffer is empty
while (buf->count == 0) {
printf("Buffer empty, consumer waiting...\n");
pthread_cond_wait(&buf->not_empty, &buf->mutex);
}
// Get item from buffer
int item = buf->buffer[buf->out];
buf->out = (buf->out + 1) % BUFFER_SIZE;
buf->count--;
printf("Consumed: %d (count: %d)\n", item, buf->count);
// Signal that buffer is not full
pthread_cond_signal(&buf->not_full);
pthread_mutex_unlock(&buf->mutex);
return item;
}
void* producer(void* arg) {
BoundedBuffer* buf = (BoundedBuffer*)arg;
for (int i = 0; i < 20; i++) {
buffer_put(buf, i);
usleep(rand() % 500000); // Random delay 0-500ms
}
return NULL;
}
void* consumer(void* arg) {
BoundedBuffer* buf = (BoundedBuffer*)arg;
for (int i = 0; i < 20; i++) {
int item = buffer_get(buf);
usleep(rand() % 800000); // Random delay 0-800ms
}
return NULL;
}
int main() {
srand(time(NULL));
BoundedBuffer buffer;
buffer_init(&buffer);
pthread_t prod_thread, cons_thread;
printf("Starting producer-consumer demo (buffer size: %d)\n\n", BUFFER_SIZE);
pthread_create(&prod_thread, NULL, producer, &buffer);
pthread_create(&cons_thread, NULL, consumer, &buffer);
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
buffer_destroy(&buffer);
printf("\nProducer-consumer finished\n");
return 0;
}
4. Read-Write Locks
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
typedef struct {
int data;
int readers_active;
int writers_active;
pthread_rwlock_t rwlock;
} SharedData;
void init_data(SharedData* sd) {
sd->data = 0;
sd->readers_active = 0;
sd->writers_active = 0;
pthread_rwlock_init(&sd->rwlock, NULL);
}
void destroy_data(SharedData* sd) {
pthread_rwlock_destroy(&sd->rwlock);
}
void* reader_thread(void* arg) {
SharedData* sd = (SharedData*)arg;
int thread_id = rand() % 100;
for (int i = 0; i < 5; i++) {
// Acquire read lock
pthread_rwlock_rdlock(&sd->rwlock);
// Reading
sd->readers_active++;
printf("Reader %d: reading value %d (readers: %d, writers: %d)\n",
thread_id, sd->data, sd->readers_active, sd->writers_active);
sd->readers_active--;
// Release read lock
pthread_rwlock_unlock(&sd->rwlock);
usleep(rand() % 500000);
}
return NULL;
}
void* writer_thread(void* arg) {
SharedData* sd = (SharedData*)arg;
int thread_id = rand() % 100;
for (int i = 0; i < 3; i++) {
// Acquire write lock
pthread_rwlock_wrlock(&sd->rwlock);
// Writing
sd->writers_active++;
sd->data += 10;
printf("Writer %d: writing value %d (readers: %d, writers: %d)\n",
thread_id, sd->data, sd->readers_active, sd->writers_active);
sd->writers_active--;
// Release write lock
pthread_rwlock_unlock(&sd->rwlock);
usleep(rand() % 800000);
}
return NULL;
}
int main() {
srand(time(NULL));
SharedData sd;
init_data(&sd);
pthread_t readers[5], writers[2];
printf("=== Read-Write Lock Demo ===\n\n");
// Create reader threads
for (int i = 0; i < 5; i++) {
pthread_create(&readers[i], NULL, reader_thread, &sd);
}
// Create writer threads
for (int i = 0; i < 2; i++) {
pthread_create(&writers[i], NULL, writer_thread, &sd);
}
// Wait for all threads
for (int i = 0; i < 5; i++) {
pthread_join(readers[i], NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(writers[i], NULL);
}
printf("\nFinal value: %d\n", sd.data);
destroy_data(&sd);
return 0;
}
Advanced Threading Patterns
1. Thread Pool
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_POOL_SIZE 4
#define TASK_QUEUE_SIZE 20
typedef struct {
void (*function)(void*);
void* argument;
} Task;
typedef struct {
Task task_queue[TASK_QUEUE_SIZE];
int queue_size;
int head;
int tail;
pthread_mutex_t mutex;
pthread_cond_t not_empty;
pthread_cond_t not_full;
pthread_t threads[THREAD_POOL_SIZE];
int shutdown;
} ThreadPool;
ThreadPool* thread_pool_create() {
ThreadPool* pool = malloc(sizeof(ThreadPool));
pool->queue_size = 0;
pool->head = 0;
pool->tail = 0;
pool->shutdown = 0;
pthread_mutex_init(&pool->mutex, NULL);
pthread_cond_init(&pool->not_empty, NULL);
pthread_cond_init(&pool->not_full, NULL);
return pool;
}
void* worker_thread(void* arg) {
ThreadPool* pool = (ThreadPool*)arg;
while (1) {
pthread_mutex_lock(&pool->mutex);
// Wait while queue is empty and not shutting down
while (pool->queue_size == 0 && !pool->shutdown) {
pthread_cond_wait(&pool->not_empty, &pool->mutex);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->mutex);
break;
}
// Get task from queue
Task task = pool->task_queue[pool->head];
pool->head = (pool->head + 1) % TASK_QUEUE_SIZE;
pool->queue_size--;
pthread_cond_signal(&pool->not_full);
pthread_mutex_unlock(&pool->mutex);
// Execute task
task.function(task.argument);
}
return NULL;
}
void thread_pool_start(ThreadPool* pool) {
for (int i = 0; i < THREAD_POOL_SIZE; i++) {
pthread_create(&pool->threads[i], NULL, worker_thread, pool);
}
}
int thread_pool_submit(ThreadPool* pool, void (*function)(void*), void* arg) {
pthread_mutex_lock(&pool->mutex);
// Wait if queue is full
while (pool->queue_size == TASK_QUEUE_SIZE) {
pthread_cond_wait(&pool->not_full, &pool->mutex);
}
// Add task to queue
pool->task_queue[pool->tail].function = function;
pool->task_queue[pool->tail].argument = arg;
pool->tail = (pool->tail + 1) % TASK_QUEUE_SIZE;
pool->queue_size++;
pthread_cond_signal(&pool->not_empty);
pthread_mutex_unlock(&pool->mutex);
return 0;
}
void thread_pool_shutdown(ThreadPool* pool) {
pthread_mutex_lock(&pool->mutex);
pool->shutdown = 1;
pthread_cond_broadcast(&pool->not_empty);
pthread_mutex_unlock(&pool->mutex);
for (int i = 0; i < THREAD_POOL_SIZE; i++) {
pthread_join(pool->threads[i], NULL);
}
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->not_empty);
pthread_cond_destroy(&pool->not_full);
free(pool);
}
// Example task functions
void print_task(void* arg) {
int* num = (int*)arg;
printf("Task %d executed by thread %ld\n", *num, pthread_self());
free(arg);
}
void compute_task(void* arg) {
int* n = (int*)arg;
int result = 0;
for (int i = 0; i <= *n; i++) {
result += i;
}
printf("Sum 0-%d = %d (thread %ld)\n", *n, result, pthread_self());
free(arg);
}
int main() {
ThreadPool* pool = thread_pool_create();
thread_pool_start(pool);
printf("=== Thread Pool Demo ===\n");
printf("Pool size: %d, Queue size: %d\n\n", THREAD_POOL_SIZE, TASK_QUEUE_SIZE);
// Submit tasks
for (int i = 0; i < 15; i++) {
int* num = malloc(sizeof(int));
*num = i + 1;
thread_pool_submit(pool, print_task, num);
}
for (int i = 0; i < 5; i++) {
int* num = malloc(sizeof(int));
*num = i * 10;
thread_pool_submit(pool, compute_task, num);
}
sleep(2); // Let tasks complete
thread_pool_shutdown(pool);
return 0;
}
2. Barrier Synchronization
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 5
#define NUM_PHASES 3
typedef struct {
pthread_barrier_t barrier;
int phase_data[NUM_THREADS];
} SharedContext;
void* worker_phase(void* arg) {
SharedContext* ctx = (SharedContext*)arg;
long thread_id = (long)arg % NUM_THREADS;
for (int phase = 0; phase < NUM_PHASES; phase++) {
// Phase 1: Compute
printf("Thread %ld: Starting phase %d\n", thread_id, phase);
usleep(rand() % 500000); // Random work
ctx->phase_data[thread_id] = rand() % 100;
printf("Thread %ld: Completed phase %d, data = %d\n",
thread_id, phase, ctx->phase_data[thread_id]);
// Wait for all threads to complete phase
pthread_barrier_wait(&ctx->barrier);
// Phase 2: Process results (only one thread does this)
if (thread_id == 0) {
int sum = 0;
for (int i = 0; i < NUM_THREADS; i++) {
sum += ctx->phase_data[i];
}
printf("Phase %d total sum: %d\n", phase, sum);
}
// Wait again before next phase
pthread_barrier_wait(&ctx->barrier);
}
return NULL;
}
int main() {
SharedContext ctx;
pthread_t threads[NUM_THREADS];
// Initialize barrier
pthread_barrier_init(&ctx.barrier, NULL, NUM_THREADS);
printf("=== Barrier Synchronization Demo ===\n");
printf("%d threads, %d phases\n\n", NUM_THREADS, NUM_PHASES);
// Create threads
for (long i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, worker_phase, (void*)i);
}
// Wait for threads
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
pthread_barrier_destroy(&ctx.barrier);
printf("\nAll phases completed\n");
return 0;
}
3. Thread-local Storage
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// Thread-local storage using __thread keyword
__thread int thread_local_counter = 0;
// Thread-specific data using pthread_key_t
pthread_key_t thread_specific_key;
typedef struct {
char name[50];
int id;
} ThreadData;
void destructor(void* value) {
if (value) {
ThreadData* data = (ThreadData*)value;
printf("Destructor: Cleaning up thread %d (%s)\n",
data->id, data->name);
free(value);
}
}
void* thread_function(void* arg) {
long thread_id = (long)arg;
// Using __thread local storage
thread_local_counter++;
printf("Thread %ld: thread_local_counter = %d\n",
thread_id, thread_local_counter);
// Using pthread_key_t thread-specific data
ThreadData* data = malloc(sizeof(ThreadData));
sprintf(data->name, "Thread-%ld", thread_id);
data->id = thread_id;
pthread_setspecific(thread_specific_key, data);
// Retrieve and use thread-specific data
ThreadData* retrieved = pthread_getspecific(thread_specific_key);
printf("Thread %ld: Retrieved data - name: %s, id: %d\n",
thread_id, retrieved->name, retrieved->id);
sleep(1);
return NULL;
}
int main() {
pthread_t threads[5];
// Create thread-specific data key
pthread_key_create(&thread_specific_key, destructor);
printf("=== Thread-Local Storage Demo ===\n\n");
// Create threads
for (long i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, thread_function, (void*)i);
}
// Wait for threads
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
// Delete key
pthread_key_delete(thread_specific_key);
return 0;
}
Common Pitfalls and Solutions
1. Deadlock Example
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_b = PTHREAD_MUTEX_INITIALIZER;
// DEADLOCK VERSION - DON'T DO THIS
void* thread1_deadlock(void* arg) {
printf("Thread 1: Locking A\n");
pthread_mutex_lock(&mutex_a);
sleep(1);
printf("Thread 1: Trying to lock B\n");
pthread_mutex_lock(&mutex_b); // DEADLOCK!
printf("Thread 1: Got both locks\n");
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
return NULL;
}
void* thread2_deadlock(void* arg) {
printf("Thread 2: Locking B\n");
pthread_mutex_lock(&mutex_b);
sleep(1);
printf("Thread 2: Trying to lock A\n");
pthread_mutex_lock(&mutex_a); // DEADLOCK!
printf("Thread 2: Got both locks\n");
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
return NULL;
}
// SOLUTION - Consistent lock ordering
void* thread1_safe(void* arg) {
printf("Thread 1 (safe): Locking A then B\n");
pthread_mutex_lock(&mutex_a);
pthread_mutex_lock(&mutex_b);
printf("Thread 1: Working...\n");
sleep(1);
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
return NULL;
}
void* thread2_safe(void* arg) {
printf("Thread 2 (safe): Locking A then B (same order)\n");
pthread_mutex_lock(&mutex_a); // Same order as thread1
pthread_mutex_lock(&mutex_b);
printf("Thread 2: Working...\n");
sleep(1);
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
return NULL;
}
int main() {
pthread_t t1, t2;
printf("=== Deadlock Demo ===\n");
printf("(Deadlock version commented out)\n\n");
// Uncomment to see deadlock
/*
printf("Creating deadlock threads...\n");
pthread_create(&t1, NULL, thread1_deadlock, NULL);
pthread_create(&t2, NULL, thread2_deadlock, NULL);
*/
printf("Creating safe threads (consistent lock ordering)...\n");
pthread_create(&t1, NULL, thread1_safe, NULL);
pthread_create(&t2, NULL, thread2_safe, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("\nSafe version completed successfully\n");
return 0;
}
2. Race Condition Detection
#include <stdio.h>
#include <pthread.h>
#include <assert.h>
#define NUM_THREADS 10
#define NUM_ITERATIONS 10000
// Shared counter with race condition
int shared_counter = 0;
// Protected counter
int protected_counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// Atomic counter (using GCC atomic builtins)
int atomic_counter = 0;
void* race_thread(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
shared_counter++; // RACE CONDITION
}
return NULL;
}
void* protected_thread(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
pthread_mutex_lock(&mutex);
protected_counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* atomic_thread(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
__sync_fetch_and_add(&atomic_counter, 1);
}
return NULL;
}
void run_test(void* (*thread_func)(void*), const char* name) {
pthread_t threads[NUM_THREADS];
if (thread_func == race_thread) {
shared_counter = 0;
} else if (thread_func == protected_thread) {
protected_counter = 0;
} else {
atomic_counter = 0;
}
printf("\nTesting %s:\n", name);
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, thread_func, NULL);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
int expected = NUM_THREADS * NUM_ITERATIONS;
int actual;
if (thread_func == race_thread) {
actual = shared_counter;
} else if (thread_func == protected_thread) {
actual = protected_counter;
} else {
actual = atomic_counter;
}
printf(" Expected: %d\n", expected);
printf(" Actual: %d\n", actual);
printf(" Difference: %d\n", expected - actual);
}
int main() {
printf("=== Race Condition Detection ===\n");
printf("Threads: %d, Iterations per thread: %d\n",
NUM_THREADS, NUM_ITERATIONS);
run_test(race_thread, "Race condition (unsafe)");
run_test(protected_thread, "Mutex protected (safe)");
run_test(atomic_thread, "Atomic operations (safe)");
pthread_mutex_destroy(&mutex);
return 0;
}
Performance Benchmarking
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#define NUM_THREADS 4
#define ARRAY_SIZE 10000000
#define NUM_RUNS 5
typedef struct {
int* array;
int start;
int end;
long long result;
} ThreadWork;
void* sum_array_worker(void* arg) {
ThreadWork* work = (ThreadWork*)arg;
work->result = 0;
for (int i = work->start; i < work->end; i++) {
work->result += work->array[i];
}
return NULL;
}
long long parallel_sum(int* array, int size, int num_threads) {
pthread_t threads[num_threads];
ThreadWork works[num_threads];
int chunk_size = size / num_threads;
// Create threads
for (int i = 0; i < num_threads; i++) {
works[i].array = array;
works[i].start = i * chunk_size;
works[i].end = (i == num_threads - 1) ? size : (i + 1) * chunk_size;
works[i].result = 0;
pthread_create(&threads[i], NULL, sum_array_worker, &works[i]);
}
// Wait for threads and sum results
long long total = 0;
for (int i = 0; i < num_threads; i++) {
pthread_join(threads[i], NULL);
total += works[i].result;
}
return total;
}
long long sequential_sum(int* array, int size) {
long long total = 0;
for (int i = 0; i < size; i++) {
total += array[i];
}
return total;
}
double get_time() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
int main() {
// Initialize array
int* array = malloc(ARRAY_SIZE * sizeof(int));
for (int i = 0; i < ARRAY_SIZE; i++) {
array[i] = rand() % 100;
}
printf("=== Performance Benchmark ===\n");
printf("Array size: %d\n", ARRAY_SIZE);
printf("Threads: %d\n", NUM_THREADS);
printf("Runs per test: %d\n\n", NUM_RUNS);
// Sequential sum benchmark
double seq_time = 0;
long long seq_result = 0;
for (int run = 0; run < NUM_RUNS; run++) {
double start = get_time();
seq_result = sequential_sum(array, ARRAY_SIZE);
double end = get_time();
seq_time += (end - start);
}
seq_time /= NUM_RUNS;
// Parallel sum benchmark
double par_time = 0;
long long par_result = 0;
for (int run = 0; run < NUM_RUNS; run++) {
double start = get_time();
par_result = parallel_sum(array, ARRAY_SIZE, NUM_THREADS);
double end = get_time();
par_time += (end - start);
}
par_time /= NUM_RUNS;
printf("Sequential sum:\n");
printf(" Result: %lld\n", seq_result);
printf(" Average time: %.4f seconds\n", seq_time);
printf("\nParallel sum (%d threads):\n", NUM_THREADS);
printf(" Result: %lld\n", par_result);
printf(" Average time: %.4f seconds\n", par_time);
printf(" Speedup: %.2fx\n", seq_time / par_time);
// Verify results
if (seq_result == par_result) {
printf("\n✓ Results match\n");
} else {
printf("\n✗ Results differ!\n");
printf(" Sequential: %lld\n", seq_result);
printf(" Parallel: %lld\n", par_result);
}
free(array);
return 0;
}
Best Practices Summary
Do's and Don'ts
// ✅ DO: Initialize synchronization primitives
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// ✅ DO: Always lock before accessing shared data
pthread_mutex_lock(&mutex);
shared_counter++;
pthread_mutex_unlock(&mutex);
// ✅ DO: Check return values
if (pthread_create(&thread, NULL, func, arg) != 0) {
perror("Failed to create thread");
}
// ✅ DO: Join or detach threads
pthread_join(thread, NULL); // Wait for thread
// OR
pthread_detach(thread); // Let thread clean up itself
// ✅ DO: Use condition variables for signaling
while (condition) {
pthread_cond_wait(&cond, &mutex);
}
// ✅ DO: Destroy synchronization objects when done
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
// ❌ DON'T: Forget to unlock mutex
pthread_mutex_lock(&mutex);
// ... do work
// Missing unlock! - will cause deadlock
// ❌ DON'T: Access shared data without locking
shared_data++; // RACE CONDITION!
// ❌ DON'T: Hold locks while sleeping
pthread_mutex_lock(&mutex);
sleep(10); // Blocks other threads unnecessarily
pthread_mutex_unlock(&mutex);
// ❌ DON'T: Use different lock ordering
// Thread 1: lock A then B
// Thread 2: lock B then A // DEADLOCK!
// ❌ DON'T: Signal condition without holding mutex
pthread_cond_signal(&cond); // Should hold mutex
Common pthread Functions Reference
| Function | Purpose |
|---|---|
pthread_create() | Create a new thread |
pthread_join() | Wait for thread termination |
pthread_exit() | Terminate calling thread |
pthread_self() | Get thread ID |
pthread_mutex_lock() | Lock mutex |
pthread_mutex_unlock() | Unlock mutex |
pthread_cond_wait() | Wait on condition |
pthread_cond_signal() | Signal one waiting thread |
pthread_cond_broadcast() | Signal all waiting threads |
pthread_rwlock_rdlock() | Acquire read lock |
pthread_rwlock_wrlock() | Acquire write lock |
pthread_barrier_wait() | Synchronize at barrier |
pthread_key_create() | Create thread-specific data key |
Conclusion
Multithreading with pthreads enables powerful parallel programming in C:
Key Concepts
- Thread creation and management - Create, join, detach threads
- Synchronization - Mutexes, condition variables, read-write locks
- Thread safety - Protect shared data from race conditions
- Deadlock prevention - Consistent lock ordering
- Performance - Leverage multiple cores for parallel processing
Best Practices
- Always synchronize access to shared data
- Use consistent lock ordering to prevent deadlocks
- Keep critical sections as small as possible
- Check return values from pthread functions
- Join or detach all created threads
- Destroy synchronization objects when done
- Consider thread safety in library design
- Profile and benchmark for optimal performance
Common Applications
- Parallel computation and data processing
- Server applications handling multiple clients
- GUI applications with background tasks
- Real-time systems with concurrent operations
- Producer-consumer problems
Mastering pthreads is essential for writing efficient, scalable, and responsive applications in C on Unix-like systems.