Multithreading is a powerful programming technique that allows multiple threads of execution to run concurrently within a single process. In C, multithreading enables programs to perform multiple operations simultaneously, improving performance on multi-core systems and creating responsive applications. This comprehensive guide covers everything you need to know to start writing multithreaded C programs using the POSIX threads (pthreads) library.
What is Multithreading?
A thread is the smallest unit of execution within a process. While a process has its own memory space, threads within the same process share that memory space, including:
- Code (instructions)
- Heap memory (dynamically allocated data)
- Global variables
- File descriptors
Each thread has its own:
- Stack (local variables)
- Program counter
- Register set
- Thread ID
┌─────────────────────────────────────┐ │ Process │ │ ┌─────────┐ ┌─────────┐ │ │ │ Thread 1│ │ Thread 2│ │ │ │ Stack │ │ Stack │ │ │ └─────────┘ └─────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Shared Heap │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Global Data │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Code │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘
Why Use Multithreading?
- Performance: Utilize multiple CPU cores for parallel processing
- Responsiveness: Keep UI responsive while performing background tasks
- Resource Sharing: Threads share memory, reducing overhead compared to processes
- Economy: Creating threads is cheaper than creating processes
- Scalability: Applications can scale with available CPU cores
The POSIX Threads (pthreads) Library
The pthreads library is the standard threading interface for Unix-like systems, including Linux and macOS. For Windows, you can use the Windows API or cross-platform libraries.
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
Compiling with pthreads:
gcc -pthread -o program program.c # or gcc -lpthread -o program program.c
Creating and Joining Threads
Basic Thread Creation:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// Thread function must return void* and take void* argument
void* thread_function(void* arg) {
int thread_num = *(int*)arg;
printf("Thread %d: Hello from thread!\n", thread_num);
// Return some value (optional)
return (void*)(long)(thread_num * 10);
}
int main() {
pthread_t thread1, thread2;
int arg1 = 1, arg2 = 2;
void* retval1, *retval2;
// Create threads
printf("Main: Creating threads...\n");
if (pthread_create(&thread1, NULL, thread_function, &arg1) != 0) {
perror("Failed to create thread 1");
exit(1);
}
if (pthread_create(&thread2, NULL, thread_function, &arg2) != 0) {
perror("Failed to create thread 2");
exit(1);
}
printf("Main: Threads created. Waiting for them to finish...\n");
// Wait for threads to complete
pthread_join(thread1, &retval1);
pthread_join(thread2, &retval2);
printf("Main: Threads finished.\n");
printf("Thread 1 returned: %ld\n", (long)retval1);
printf("Thread 2 returned: %ld\n", (long)retval2);
return 0;
}
Thread Attributes
You can customize thread behavior using thread attributes:
#include <pthread.h>
#include <stdio.h>
void* thread_func(void* arg) {
printf("Thread running with custom attributes\n");
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
// Initialize attribute object
pthread_attr_init(&attr);
// Set detached state (thread will clean up itself when done)
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// Set stack size (optional)
pthread_attr_setstacksize(&attr, 1024 * 1024); // 1 MB stack
// Create thread with attributes
if (pthread_create(&thread, &attr, thread_func, NULL) != 0) {
perror("Failed to create thread");
return 1;
}
// Destroy attribute object (no longer needed)
pthread_attr_destroy(&attr);
// Since thread is detached, we don't need to join
printf("Main: Thread created in detached state\n");
// Give thread time to run
sleep(1);
return 0;
}
Thread Synchronization
1. Mutexes (Mutual Exclusion)
Mutexes protect shared data from concurrent access:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 5
#define NUM_INCREMENTS 1000000
// Shared counter
long long counter = 0;
// Mutex for protecting counter
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void* increment_counter(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < NUM_INCREMENTS; i++) {
// Lock mutex before accessing shared counter
pthread_mutex_lock(&counter_mutex);
counter++;
pthread_mutex_unlock(&counter_mutex);
}
printf("Thread %d finished\n", thread_id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
if (pthread_create(&threads[i], NULL, increment_counter, &thread_ids[i]) != 0) {
perror("Failed to create thread");
exit(1);
}
}
// Wait for all threads to finish
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// Expected: NUM_THREADS * NUM_INCREMENTS
printf("Final counter value: %lld\n", counter);
printf("Expected value: %d\n", NUM_THREADS * NUM_INCREMENTS);
// Destroy mutex
pthread_mutex_destroy(&counter_mutex);
return 0;
}
2. Mutex with Error Checking
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
void demonstrate_mutex_errors() {
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
// Initialize mutex with error checking
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&mutex, &attr);
// First lock - should succeed
int ret = pthread_mutex_lock(&mutex);
printf("First lock: %s\n", ret == 0 ? "success" : "failed");
// Second lock by same thread - would deadlock with normal mutex,
// but with error checking it returns EDEADLK
ret = pthread_mutex_lock(&mutex);
if (ret == EDEADLK) {
printf("Second lock detected deadlock (as expected)\n");
}
// Unlock once
pthread_mutex_unlock(&mutex);
// Try to unlock without holding lock
ret = pthread_mutex_unlock(&mutex);
if (ret == EPERM) {
printf("Unlock without lock detected (as expected)\n");
}
pthread_mutex_destroy(&mutex);
pthread_mutexattr_destroy(&attr);
}
3. Condition Variables
Condition variables allow threads to wait for certain conditions:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Shared data
int buffer = 0;
int data_ready = 0;
// Synchronization primitives
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// Producer thread
void* producer(void* arg) {
int produced = 0;
while (produced < 5) {
// Simulate work
sleep(1);
pthread_mutex_lock(&mutex);
// Produce data
buffer = rand() % 100;
printf("Producer: Produced %d\n", buffer);
// Signal that data is ready
data_ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
produced++;
}
return NULL;
}
// Consumer thread
void* consumer(void* arg) {
int consumed = 0;
while (consumed < 5) {
pthread_mutex_lock(&mutex);
// Wait for data to be ready
while (!data_ready) {
// Wait unlocks mutex and blocks, then reacquires when signaled
pthread_cond_wait(&cond, &mutex);
}
// Consume data
printf("Consumer: Consumed %d\n", buffer);
data_ready = 0;
pthread_mutex_unlock(&mutex);
consumed++;
}
return NULL;
}
int main() {
pthread_t prod, cons;
// Create threads
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
// Wait for completion
pthread_join(prod, NULL);
pthread_join(cons, NULL);
// Cleanup
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
4. Broadcast vs. Signal
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int condition_met = 0;
void* waiter(void* arg) {
int id = *(int*)arg;
pthread_mutex_lock(&mutex);
while (!condition_met) {
printf("Waiter %d: Waiting...\n", id);
pthread_cond_wait(&cond, &mutex);
}
printf("Waiter %d: Condition met! Proceeding.\n", id);
pthread_mutex_unlock(&mutex);
return NULL;
}
void* signaler(void* arg) {
sleep(2);
pthread_mutex_lock(&mutex);
condition_met = 1;
// Try both signal and broadcast
// pthread_cond_signal(&cond); // Wakes only one thread
pthread_cond_broadcast(&cond); // Wakes all waiting threads
printf("Signaler: Condition signaled\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t waiters[3];
pthread_t sig;
int ids[3] = {1, 2, 3};
// Create waiting threads
for (int i = 0; i < 3; i++) {
pthread_create(&waiters[i], NULL, waiter, &ids[i]);
}
// Create signaler thread
pthread_create(&sig, NULL, signaler, NULL);
// Wait for all threads
for (int i = 0; i < 3; i++) {
pthread_join(waiters[i], NULL);
}
pthread_join(sig, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
Thread-Safe Data Structures
Thread-Safe Queue Example:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct ThreadSafeQueue {
Node* head;
Node* tail;
int size;
pthread_mutex_t mutex;
pthread_cond_t not_empty;
pthread_cond_t not_full;
int max_size;
} ThreadSafeQueue;
// Initialize queue
void queue_init(ThreadSafeQueue* queue, int max_size) {
queue->head = NULL;
queue->tail = NULL;
queue->size = 0;
queue->max_size = max_size;
pthread_mutex_init(&queue->mutex, NULL);
pthread_cond_init(&queue->not_empty, NULL);
pthread_cond_init(&queue->not_full, NULL);
}
// Destroy queue
void queue_destroy(ThreadSafeQueue* queue) {
pthread_mutex_lock(&queue->mutex);
// Free all nodes
Node* current = queue->head;
while (current != NULL) {
Node* temp = current;
current = current->next;
free(temp);
}
pthread_mutex_unlock(&queue->mutex);
pthread_mutex_destroy(&queue->mutex);
pthread_cond_destroy(&queue->not_empty);
pthread_cond_destroy(&queue->not_full);
}
// Enqueue (producer)
void queue_enqueue(ThreadSafeQueue* queue, int value) {
pthread_mutex_lock(&queue->mutex);
// Wait if queue is full
while (queue->size >= queue->max_size) {
pthread_cond_wait(&queue->not_full, &queue->mutex);
}
// Create new node
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = value;
new_node->next = NULL;
// Add to queue
if (queue->tail == NULL) {
queue->head = new_node;
queue->tail = new_node;
} else {
queue->tail->next = new_node;
queue->tail = new_node;
}
queue->size++;
printf("Enqueued: %d (queue size: %d)\n", value, queue->size);
// Signal that queue is not empty
pthread_cond_signal(&queue->not_empty);
pthread_mutex_unlock(&queue->mutex);
}
// Dequeue (consumer)
int queue_dequeue(ThreadSafeQueue* queue) {
pthread_mutex_lock(&queue->mutex);
// Wait if queue is empty
while (queue->head == NULL) {
pthread_cond_wait(&queue->not_empty, &queue->mutex);
}
// Remove from queue
Node* temp = queue->head;
int value = temp->data;
queue->head = queue->head->next;
if (queue->head == NULL) {
queue->tail = NULL;
}
queue->size--;
free(temp);
printf("Dequeued: %d (queue size: %d)\n", value, queue->size);
// Signal that queue is not full
pthread_cond_signal(&queue->not_full);
pthread_mutex_unlock(&queue->mutex);
return value;
}
// Producer thread
void* producer_thread(void* arg) {
ThreadSafeQueue* queue = (ThreadSafeQueue*)arg;
for (int i = 0; i < 10; i++) {
queue_enqueue(queue, i);
usleep(rand() % 500000); // Random delay
}
return NULL;
}
// Consumer thread
void* consumer_thread(void* arg) {
ThreadSafeQueue* queue = (ThreadSafeQueue*)arg;
for (int i = 0; i < 10; i++) {
int value = queue_dequeue(queue);
usleep(rand() % 500000); // Random delay
}
return NULL;
}
int main() {
ThreadSafeQueue queue;
queue_init(&queue, 5); // Max size 5
pthread_t producer, consumer;
// Create producer and consumer
pthread_create(&producer, NULL, producer_thread, &queue);
pthread_create(&consumer, NULL, consumer_thread, &queue);
// Wait for completion
pthread_join(producer, NULL);
pthread_join(consumer, NULL);
// Cleanup
queue_destroy(&queue);
return 0;
}
Thread-Local Storage (TLS)
Each thread can have its own private data:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// Thread-local storage using __thread keyword (GCC)
__thread int thread_local_counter = 0;
// Alternative: pthread keys
pthread_key_t thread_key;
void destructor(void* value) {
free(value);
printf("Thread-local data freed\n");
}
void* thread_function(void* arg) {
int thread_id = *(int*)arg;
// Using __thread variable
thread_local_counter = thread_id * 100;
printf("Thread %d: __thread counter = %d\n",
thread_id, thread_local_counter);
// Using pthread_key
int* data = (int*)malloc(sizeof(int));
*data = thread_id * 1000;
pthread_setspecific(thread_key, data);
int* retrieved = (int*)pthread_getspecific(thread_key);
printf("Thread %d: pthread_key data = %d\n",
thread_id, *retrieved);
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {1, 2, 3};
// Create thread-local key
pthread_key_create(&thread_key, destructor);
// Create threads
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &ids[i]);
}
// Wait for threads
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
// Cleanup
pthread_key_delete(thread_key);
return 0;
}
Thread Pools
A thread pool manages a group of worker threads to execute tasks:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct Task {
void (*function)(void*);
void* argument;
struct Task* next;
} Task;
typedef struct ThreadPool {
pthread_t* threads;
int thread_count;
Task* task_queue_head;
Task* task_queue_tail;
int queue_size;
pthread_mutex_t queue_mutex;
pthread_cond_t queue_not_empty;
pthread_cond_t queue_not_full;
int shutdown;
int max_queue_size;
} ThreadPool;
// Worker thread function
void* worker_thread(void* arg) {
ThreadPool* pool = (ThreadPool*)arg;
while (1) {
pthread_mutex_lock(&pool->queue_mutex);
// Wait for task or shutdown
while (pool->task_queue_head == NULL && !pool->shutdown) {
pthread_cond_wait(&pool->queue_not_empty, &pool->queue_mutex);
}
if (pool->shutdown && pool->task_queue_head == NULL) {
pthread_mutex_unlock(&pool->queue_mutex);
break;
}
// Get task from queue
Task* task = pool->task_queue_head;
pool->task_queue_head = pool->task_queue_head->next;
if (pool->task_queue_head == NULL) {
pool->task_queue_tail = NULL;
}
pool->queue_size--;
// Signal that queue is not full
pthread_cond_signal(&pool->queue_not_full);
pthread_mutex_unlock(&pool->queue_mutex);
// Execute task
task->function(task->argument);
free(task);
}
return NULL;
}
// Initialize thread pool
ThreadPool* thread_pool_create(int thread_count, int max_queue_size) {
ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
pool->thread_count = thread_count;
pool->threads = (pthread_t*)malloc(thread_count * sizeof(pthread_t));
pool->task_queue_head = NULL;
pool->task_queue_tail = NULL;
pool->queue_size = 0;
pool->max_queue_size = max_queue_size;
pool->shutdown = 0;
pthread_mutex_init(&pool->queue_mutex, NULL);
pthread_cond_init(&pool->queue_not_empty, NULL);
pthread_cond_init(&pool->queue_not_full, NULL);
// Create worker threads
for (int i = 0; i < thread_count; i++) {
pthread_create(&pool->threads[i], NULL, worker_thread, pool);
}
return pool;
}
// Add task to pool
int thread_pool_add_task(ThreadPool* pool, void (*function)(void*), void* argument) {
pthread_mutex_lock(&pool->queue_mutex);
// Wait if queue is full
while (pool->queue_size >= pool->max_queue_size && !pool->shutdown) {
pthread_cond_wait(&pool->queue_not_full, &pool->queue_mutex);
}
if (pool->shutdown) {
pthread_mutex_unlock(&pool->queue_mutex);
return -1;
}
// Create task
Task* task = (Task*)malloc(sizeof(Task));
task->function = function;
task->argument = argument;
task->next = NULL;
// Add to queue
if (pool->task_queue_tail == NULL) {
pool->task_queue_head = task;
pool->task_queue_tail = task;
} else {
pool->task_queue_tail->next = task;
pool->task_queue_tail = task;
}
pool->queue_size++;
// Signal that queue is not empty
pthread_cond_signal(&pool->queue_not_empty);
pthread_mutex_unlock(&pool->queue_mutex);
return 0;
}
// Shutdown thread pool
void thread_pool_shutdown(ThreadPool* pool) {
pthread_mutex_lock(&pool->queue_mutex);
pool->shutdown = 1;
// Wake up all workers
pthread_cond_broadcast(&pool->queue_not_empty);
pthread_mutex_unlock(&pool->queue_mutex);
// Wait for all workers to finish
for (int i = 0; i < pool->thread_count; i++) {
pthread_join(pool->threads[i], NULL);
}
// Clean up remaining tasks
pthread_mutex_lock(&pool->queue_mutex);
Task* current = pool->task_queue_head;
while (current != NULL) {
Task* temp = current;
current = current->next;
free(temp);
}
pthread_mutex_unlock(&pool->queue_mutex);
// Cleanup
pthread_mutex_destroy(&pool->queue_mutex);
pthread_cond_destroy(&pool->queue_not_empty);
pthread_cond_destroy(&pool->queue_not_full);
free(pool->threads);
free(pool);
}
// Example task function
void print_number(void* arg) {
int num = *(int*)arg;
printf("Task: %d (thread %lu)\n", num, pthread_self());
free(arg); // Free allocated argument
usleep(100000); // Simulate work
}
int main() {
// Create thread pool with 4 threads and max queue size 10
ThreadPool* pool = thread_pool_create(4, 10);
// Add 20 tasks
for (int i = 0; i < 20; i++) {
int* num = (int*)malloc(sizeof(int));
*num = i;
thread_pool_add_task(pool, print_number, num);
}
// Give tasks time to complete
sleep(3);
// Shutdown pool
thread_pool_shutdown(pool);
return 0;
}
Common Pitfalls and Best Practices
1. Deadlock Prevention
#include <pthread.h>
#include <stdio.h>
// Example of deadlock and prevention
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
// BAD: Can deadlock
void* bad_worker1(void* arg) {
pthread_mutex_lock(&mutex1);
printf("Worker1: locked mutex1\n");
sleep(1);
pthread_mutex_lock(&mutex2); // May deadlock if worker2 has mutex2
printf("Worker1: locked mutex2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* bad_worker2(void* arg) {
pthread_mutex_lock(&mutex2);
printf("Worker2: locked mutex2\n");
sleep(1);
pthread_mutex_lock(&mutex1); // May deadlock if worker1 has mutex1
printf("Worker2: locked mutex1\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
// GOOD: Consistent lock ordering prevents deadlock
void* good_worker1(void* arg) {
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
printf("Worker1: locked both mutexes in order\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* good_worker2(void* arg) {
// Same locking order as worker1
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
printf("Worker2: locked both mutexes in order\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
2. Trylock to Avoid Deadlock
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t mutex_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_b = PTHREAD_MUTEX_INITIALIZER;
void* trylock_worker(void* arg) {
int id = *(int*)arg;
while (1) {
// Try to lock first mutex
pthread_mutex_lock(&mutex_a);
// Try to lock second mutex without blocking
if (pthread_mutex_trylock(&mutex_b) == 0) {
printf("Thread %d: Got both locks\n", id);
// Do work
sleep(1);
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
break;
} else {
// Couldn't get second lock, release first and try again
pthread_mutex_unlock(&mutex_a);
printf("Thread %d: Couldn't get both locks, retrying\n", id);
usleep(rand() % 100000); // Random backoff
}
}
return NULL;
}
3. Thread Safety Rules
#include <pthread.h>
#include <stdio.h>
#include <string.h>
// Thread-safe function with local data
char* thread_safe_strerror(int errnum) {
static __thread char buffer[256]; // Thread-local storage
// Actual error string generation
snprintf(buffer, sizeof(buffer), "Error %d", errnum);
return buffer;
}
// Thread-safe counter
typedef struct {
pthread_mutex_t mutex;
long long value;
} AtomicCounter;
void atomic_increment(AtomicCounter* counter) {
pthread_mutex_lock(&counter->mutex);
counter->value++;
pthread_mutex_unlock(&counter->mutex);
}
long long atomic_get(AtomicCounter* counter) {
pthread_mutex_lock(&counter->mutex);
long long val = counter->value;
pthread_mutex_unlock(&counter->mutex);
return val;
}
Performance Considerations
#include <pthread.h>
#include <stdio.h>
#include <time.h>
// Measure thread creation overhead
void measure_thread_overhead() {
struct timespec start, end;
pthread_t thread;
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < 1000; i++) {
pthread_create(&thread, NULL, thread_function, NULL);
pthread_join(thread, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
double elapsed = (end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9;
printf("Average thread creation+join time: %.3f ms\n",
elapsed * 1000 / 1000);
}
// False sharing demonstration
typedef struct {
int counter; // Multiple counters in same cache line
int counter2; // can cause false sharing
} BadCounter;
typedef struct {
int counter;
char padding[64]; // Pad to separate cache lines
int counter2;
} GoodCounter;
Complete Example: Parallel Matrix Multiplication
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 1000
#define NUM_THREADS 4
typedef struct {
int thread_id;
int start_row;
int end_row;
int (*A)[SIZE];
int (*B)[SIZE];
int (*C)[SIZE];
} ThreadData;
// Matrix multiplication for a range of rows
void* multiply_rows(void* arg) {
ThreadData* data = (ThreadData*)arg;
printf("Thread %d: processing rows %d to %d\n",
data->thread_id, data->start_row, data->end_row - 1);
for (int i = data->start_row; i < data->end_row; i++) {
for (int j = 0; j < SIZE; j++) {
int sum = 0;
for (int k = 0; k < SIZE; k++) {
sum += data->A[i][k] * data->B[k][j];
}
data->C[i][j] = sum;
}
}
return NULL;
}
int main() {
// Allocate matrices
int (*A)[SIZE] = malloc(SIZE * sizeof(*A));
int (*B)[SIZE] = malloc(SIZE * sizeof(*B));
int (*C)[SIZE] = malloc(SIZE * sizeof(*C));
if (!A || !B || !C) {
perror("Failed to allocate matrices");
return 1;
}
// Initialize matrices
srand(time(NULL));
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
A[i][j] = rand() % 10;
B[i][j] = rand() % 10;
}
}
// Create thread data
pthread_t threads[NUM_THREADS];
ThreadData thread_data[NUM_THREADS];
int rows_per_thread = SIZE / NUM_THREADS;
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
thread_data[i].thread_id = i;
thread_data[i].start_row = i * rows_per_thread;
thread_data[i].end_row = (i == NUM_THREADS - 1) ?
SIZE : (i + 1) * rows_per_thread;
thread_data[i].A = A;
thread_data[i].B = B;
thread_data[i].C = C;
pthread_create(&threads[i], NULL, multiply_rows, &thread_data[i]);
}
// Wait for all threads
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
double elapsed = (end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9;
printf("Matrix multiplication completed in %.3f seconds\n", elapsed);
printf("Result check (C[0][0] = %d)\n", C[0][0]);
// Cleanup
free(A);
free(B);
free(C);
return 0;
}
Debugging Multithreaded Programs
#include <pthread.h>
#include <stdio.h>
#include <signal.h>
// Thread sanitizer (compile with -fsanitize=thread)
// gcc -fsanitize=thread -g -pthread program.c
// Helgrind (Valgrind tool)
// valgrind --tool=helgrind ./program
// GDB debugging with threads
void gdb_thread_commands() {
printf("GDB commands for threads:\n");
printf(" info threads - List all threads\n");
printf(" thread <num> - Switch to thread <num>\n");
printf(" thread apply all bt - Backtrace all threads\n");
printf(" set scheduler-locking on - Prevent thread switching\n");
}
// Signal handling in multithreaded programs
pthread_mutex_t signal_mutex = PTHREAD_MUTEX_INITIALIZER;
void signal_handler(int sig) {
// Only async-signal-safe functions here
write(STDERR_FILENO, "Signal received\n", 16);
}
void setup_signal_handling() {
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
}
Summary: pthread Functions Reference
| Function | Purpose |
|---|---|
pthread_create() | Create a new thread |
pthread_join() | Wait for thread termination |
pthread_detach() | Make thread detached (auto-cleanup) |
pthread_self() | Get current thread ID |
pthread_equal() | Compare thread IDs |
pthread_exit() | Exit current thread |
pthread_mutex_init() | Initialize mutex |
pthread_mutex_lock() | Lock mutex |
pthread_mutex_unlock() | Unlock mutex |
pthread_mutex_trylock() | Attempt to lock mutex |
pthread_mutex_destroy() | Destroy mutex |
pthread_cond_init() | Initialize condition variable |
pthread_cond_wait() | Wait on condition |
pthread_cond_signal() | Signal one waiting thread |
pthread_cond_broadcast() | Signal all waiting threads |
pthread_cond_destroy() | Destroy condition variable |
pthread_key_create() | Create thread-local storage key |
pthread_setspecific() | Set thread-local value |
pthread_getspecific() | Get thread-local value |
pthread_key_delete() | Delete thread-local key |
Conclusion
Multithreading in C using pthreads provides a powerful foundation for building concurrent applications. By understanding thread creation, synchronization mechanisms (mutexes, condition variables), and thread-safe data structures, you can create efficient, scalable programs that fully utilize modern multi-core processors.
Remember these key principles:
- Always protect shared data with mutexes
- Avoid deadlocks by establishing consistent lock ordering
- Use condition variables for thread coordination
- Be aware of thread safety in function design
- Profile and test thoroughly, as threading bugs can be subtle
With practice, these concepts become second nature, enabling you to harness the full power of concurrent programming in C.