Multithreading with pthreads in C: Complete Guide

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

FunctionPurpose
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

  1. Always synchronize access to shared data
  2. Use consistent lock ordering to prevent deadlocks
  3. Keep critical sections as small as possible
  4. Check return values from pthread functions
  5. Join or detach all created threads
  6. Destroy synchronization objects when done
  7. Consider thread safety in library design
  8. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper