Mastering Concurrency: A Comprehensive Guide to Multithreading Basics in C

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?

  1. Performance: Utilize multiple CPU cores for parallel processing
  2. Responsiveness: Keep UI responsive while performing background tasks
  3. Resource Sharing: Threads share memory, reducing overhead compared to processes
  4. Economy: Creating threads is cheaper than creating processes
  5. 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

FunctionPurpose
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.

Leave a Reply

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


Macro Nepal Helper