POSIX Concurrency in C - Complete Guide
Table of Contents
1. Multithreaded Programming Basics
2. Basic Thread Creation Example
3. Thread Synchronization
4. Debugging Multithreaded Applications
5. Best Practices and Coding Guidelines
6. Complete Examples
1. Multithreaded Programming Basics {#basics}
Thread vs Process
Aspect Process Thread
Memory Space Separate address space Shared address space
Creation Overhead High (fork system call) Low (pthread_create)
Communication IPC mechanisms required Direct memory sharing
Context Switching Expensive Cheaper
Crash Impact Isolated Can affect entire process
Thread Attributes
Detached vs Joinable: Joinable threads must be joined; detached threads clean up automatically
Stack Size: Can be configured (default varies by system)
Scheduling Policy: SCHED_FIFO, SCHED_RR, SCHED_OTHER
Priority: Thread scheduling priority
Shared Resources
Threads within a process share:
Global variables
Heap memory
File descriptors
Signal handlers
Process ID
Each thread has its own:
Stack
Registers
Program counter
Thread Standards
POSIX Threads (pthreads): IEEE POSIX 1003.1c standard
System V Threads: Older Solaris/SVR4 standard (largely obsolete)
2. Basic Thread Creation Example {#basic-example}
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
// Thread parameter structure (heap-allocated)
typedef struct {
int thread_id;
char message[100];
} thread_data_t;
// Thread function
void* worker_thread(void* arg) {
thread_data_t* data = (thread_data_t*)arg;
printf("Thread %d started: %s\n", data->thread_id, data->message);
// Simulate work
sleep(2);
// Allocate return value on heap
int* result = malloc(sizeof(int));
if (!result) {
pthread_exit(NULL);
}
*result = data->thread_id * 10;
printf("Thread %d finished\n", data->thread_id);
pthread_exit(result); // Use pthread_exit instead of return
}
int main() {
const int NUM_THREADS = 3;
pthread_t threads[NUM_THREADS];
thread_data_t* thread_data[NUM_THREADS];
int rc;
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
// Allocate thread data on heap (not stack)
thread_data[i] = malloc(sizeof(thread_data_t));
if (!thread_data[i]) {
fprintf(stderr, "Failed to allocate memory for thread data\n");
exit(1);
}
thread_data[i]->thread_id = i;
snprintf(thread_data[i]->message, sizeof(thread_data[i]->message),
"Hello from thread %d", i);
rc = pthread_create(&threads[i], NULL, worker_thread, thread_data[i]);
if (rc) {
fprintf(stderr, "Error creating thread %d: %s\n", i, strerror(rc));
exit(1);
}
}
// Wait for all threads to complete
for (int i = 0; i < NUM_THREADS; i++) {
void* result;
rc = pthread_join(threads[i], &result);
if (rc) {
fprintf(stderr, "Error joining thread %d: %s\n", i, strerror(rc));
} else if (result) {
printf("Thread %d returned: %d\n", i, *(int*)result);
free(result); // Free the result allocated by thread
}
// Free thread data
free(thread_data[i]);
}
printf("All threads completed\n");
return 0;
}
3. Thread Synchronization {#synchronization}
Race Conditions and Critical Sections
A race condition occurs when multiple threads access shared data simultaneously, and the final result
depends on the timing of their execution.
A critical section is a code segment that accesses shared resources and must be executed atomically.
Synchronization Mechanisms
Mutex (Mutual Exclusion)
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
// Global shared resource
int global_counter = 0;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
typedef struct {
int thread_id;
int iterations;
} thread_param_t;
void* increment_counter(void* arg) {
thread_param_t* param = (thread_param_t*)arg;
for (int i = 0; i < param->iterations; i++) {
// Lock mutex before accessing shared resource
int rc = pthread_mutex_lock(&counter_mutex);
if (rc) {
fprintf(stderr, "Mutex lock failed: %s\n", strerror(rc));
pthread_exit(NULL);
}
// Critical section - keep it short
int temp = global_counter;
usleep(1); // Simulate some processing
global_counter = temp + 1;
// Always unlock mutex
rc = pthread_mutex_unlock(&counter_mutex);
if (rc) {
fprintf(stderr, "Mutex unlock failed: %s\n", strerror(rc));
pthread_exit(NULL);
}
// Some work outside critical section
usleep(10);
}
printf("Thread %d completed %d increments\n",
param->thread_id, param->iterations);
pthread_exit(NULL);
}
int main() {
const int NUM_THREADS = 5;
const int ITERATIONS_PER_THREAD = 1000;
pthread_t threads[NUM_THREADS];
thread_param_t* params[NUM_THREADS];
int rc;
printf("Initial counter value: %d\n", global_counter);
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
params[i] = malloc(sizeof(thread_param_t));
if (!params[i]) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
params[i]->thread_id = i;
params[i]->iterations = ITERATIONS_PER_THREAD;
rc = pthread_create(&threads[i], NULL, increment_counter, params[i]);
if (rc) {
fprintf(stderr, "Thread creation failed: %s\n", strerror(rc));
exit(1);
}
}
// Wait for all threads
for (int i = 0; i < NUM_THREADS; i++) {
rc = pthread_join(threads[i], NULL);
if (rc) {
fprintf(stderr, "Thread join failed: %s\n", strerror(rc));
}
free(params[i]);
}
printf("Final counter value: %d\n", global_counter);
printf("Expected value: %d\n", NUM_THREADS * ITERATIONS_PER_THREAD);
// Cleanup mutex
pthread_mutex_destroy(&counter_mutex);
return 0;
}
Semaphores
c
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 5
#define NUM_PRODUCERS 2
#define NUM_CONSUMERS 2
#define ITEMS_TO_PRODUCE 10
// Shared buffer
int buffer[BUFFER_SIZE];
int buffer_index = 0;
// Semaphores and mutex
sem_t empty_slots; // Number of empty slots
sem_t full_slots; // Number of full slots
pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
typedef struct {
int id;
int items_to_process;
} worker_param_t;
void* producer(void* arg) {
worker_param_t* param = (worker_param_t*)arg;
for (int i = 0; i < param->items_to_process; i++) {
int item = param->id * 100 + i; // Unique item ID
// Wait for empty slot
if (sem_wait(&empty_slots) != 0) {
perror("sem_wait(empty_slots)");
pthread_exit(NULL);
}
// Lock buffer
if (pthread_mutex_lock(&buffer_mutex) != 0) {
perror("pthread_mutex_lock");
pthread_exit(NULL);
}
// Add item to buffer
buffer[buffer_index] = item;
printf("Producer %d produced item %d at index %d\n",
param->id, item, buffer_index);
buffer_index++;
// Unlock buffer
if (pthread_mutex_unlock(&buffer_mutex) != 0) {
perror("pthread_mutex_unlock");
pthread_exit(NULL);
}
// Signal full slot
if (sem_post(&full_slots) != 0) {
perror("sem_post(full_slots)");
pthread_exit(NULL);
}
usleep(100000); // 100ms delay
}
pthread_exit(NULL);
}
void* consumer(void* arg) {
worker_param_t* param = (worker_param_t*)arg;
for (int i = 0; i < param->items_to_process; i++) {
// Wait for full slot
if (sem_wait(&full_slots) != 0) {
perror("sem_wait(full_slots)");
pthread_exit(NULL);
}
// Lock buffer
if (pthread_mutex_lock(&buffer_mutex) != 0) {
perror("pthread_mutex_lock");
pthread_exit(NULL);
}
// Remove item from buffer
buffer_index--;
int item = buffer[buffer_index];
printf("Consumer %d consumed item %d from index %d\n",
param->id, item, buffer_index);
// Unlock buffer
if (pthread_mutex_unlock(&buffer_mutex) != 0) {
perror("pthread_mutex_unlock");
pthread_exit(NULL);
}
// Signal empty slot
if (sem_post(&empty_slots) != 0) {
perror("sem_post(empty_slots)");
pthread_exit(NULL);
}
usleep(150000); // 150ms delay
}
pthread_exit(NULL);
}
int main() {
pthread_t producers[NUM_PRODUCERS];
pthread_t consumers[NUM_CONSUMERS];
worker_param_t* producer_params[NUM_PRODUCERS];
worker_param_t* consumer_params[NUM_CONSUMERS];
// Initialize semaphores
if (sem_init(&empty_slots, 0, BUFFER_SIZE) != 0) {
perror("sem_init(empty_slots)");
exit(1);
}
if (sem_init(&full_slots, 0, 0) != 0) {
perror("sem_init(full_slots)");
exit(1);
}
// Create producers
for (int i = 0; i < NUM_PRODUCERS; i++) {
producer_params[i] = malloc(sizeof(worker_param_t));
producer_params[i]->id = i;
producer_params[i]->items_to_process = ITEMS_TO_PRODUCE;
if (pthread_create(&producers[i], NULL, producer, producer_params[i]) != 0) {
perror("pthread_create(producer)");
exit(1);
}
}
// Create consumers
for (int i = 0; i < NUM_CONSUMERS; i++) {
consumer_params[i] = malloc(sizeof(worker_param_t));
consumer_params[i]->id = i;
consumer_params[i]->items_to_process = ITEMS_TO_PRODUCE;
if (pthread_create(&consumers[i], NULL, consumer, consumer_params[i]) != 0) {
perror("pthread_create(consumer)");
exit(1);
}
}
// Wait for all threads
for (int i = 0; i < NUM_PRODUCERS; i++) {
pthread_join(producers[i], NULL);
free(producer_params[i]);
}
for (int i = 0; i < NUM_CONSUMERS; i++) {
pthread_join(consumers[i], NULL);
free(consumer_params[i]);
}
// Cleanup
sem_destroy(&empty_slots);
sem_destroy(&full_slots);
pthread_mutex_destroy(&buffer_mutex);
printf("Program completed successfully\n");
return 0;
}
4. Debugging Multithreaded Applications {#debugging}
Using GDB for Thread Debugging
Compilation for Debugging
bash
gcc -g -pthread -o myprogram myprogram.c
Key GDB Commands for Threads
bash
# List all threads
(gdb) info threads
# Switch to specific thread
(gdb) thread 2
# Apply command to all threads
(gdb) thread apply all bt
# Set breakpoint in specific thread
(gdb) break function_name thread 2
# Continue only current thread
(gdb) continue
# Step only current thread
(gdb) set scheduler-locking step
Using Valgrind for Memory Issues
Helgrind - Race Condition Detection
bash
valgrind --tool=helgrind ./myprogram
DRD - Data Race Detection
bash
valgrind --tool=drd ./myprogram
Memcheck - Memory Leak Detection
bash
valgrind --leak-check=full --track-origins=yes ./myprogram
Common Issues and Solutions
1. Deadlock Detection Example
c
// Problematic code that can cause deadlock
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void* thread1_func(void* arg) {
pthread_mutex_lock(&mutex1);
sleep(1);
pthread_mutex_lock(&mutex2); // Potential deadlock
// Critical section
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2_func(void* arg) {
pthread_mutex_lock(&mutex2);
sleep(1);
pthread_mutex_lock(&mutex1); // Potential deadlock
// Critical section
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
// Solution: Always acquire locks in the same order
void* thread1_func_fixed(void* arg) {
pthread_mutex_lock(&mutex1); // Always lock mutex1 first
pthread_mutex_lock(&mutex2);
// Critical section
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
5. Best Practices and Coding Guidelines {#guidelines}
Error Handling
c
// Always check return values
int rc = pthread_create(&thread, NULL, thread_func, arg);
if (rc) {
fprintf(stderr, "pthread_create failed: %s\n", strerror(rc));
exit(1);
}
rc = pthread_join(thread, NULL);
if (rc) {
fprintf(stderr, "pthread_join failed: %s\n", strerror(rc));
}
Memory Management
c
// ✅ CORRECT: Pass heap-allocated data
typedef struct {
int data;
} thread_param_t;
void* correct_thread(void* arg) {
thread_param_t* param = (thread_param_t*)arg;
// Use param->data
// Return heap-allocated or static data
int* result = malloc(sizeof(int));
*result = 42;
pthread_exit(result);
}
int main() {
pthread_t thread;
thread_param_t* param = malloc(sizeof(thread_param_t));
param->data = 100;
pthread_create(&thread, NULL, correct_thread, param);
void* result;
pthread_join(thread, &result);
printf("Result: %d\n", *(int*)result);
free(result); // Free thread's return value
free(param); // Free parameter
}
// ❌ WRONG: Passing stack variables
void wrong_example() {
pthread_t thread;
int stack_var = 100; // Stack variable
// DON'T DO THIS - stack_var may be invalid when thread runs
pthread_create(&thread, NULL, some_thread, &stack_var);
}
Mutex Best Practices
c
// ✅ CORRECT: Short critical sections
void good_mutex_usage() {
pthread_mutex_lock(&mutex);
// Short critical section
shared_variable++;
pthread_mutex_unlock(&mutex);
// Do expensive work outside critical section
expensive_computation();
}
// ❌ WRONG: Long critical sections
void bad_mutex_usage() {
pthread_mutex_lock(&mutex);
shared_variable++;
expensive_computation(); // Don't do this inside lock
file_operations(); // Don't do this inside lock
pthread_mutex_unlock(&mutex);
}
// ✅ CORRECT: Proper error handling and cleanup
int safe_mutex_operation() {
int rc = pthread_mutex_lock(&mutex);
if (rc) {
fprintf(stderr, "Mutex lock failed: %s\n", strerror(rc));
return -1;
}
// Critical section
shared_variable++;
rc = pthread_mutex_unlock(&mutex);
if (rc) {
fprintf(stderr, "Mutex unlock failed: %s\n", strerror(rc));
return -1;
}
return 0;
}
Thread Lifecycle Management
void proper_thread_management() {
const int NUM_THREADS = 5;
pthread_t threads[NUM_THREADS];
// Create all threads
for (int i = 0; i < NUM_THREADS; i++) {
// ... create threads with error checking
}
// Wait for ALL threads before exiting
for (int i = 0; i < NUM_THREADS; i++) {
int rc = pthread_join(threads[i], NULL);
if (rc) {
fprintf(stderr, "Failed to join thread %d: %s\n", i, strerror(rc));
}
}
// Cleanup resources
pthread_mutex_destroy(&mutex);
}
6. Complete Examples {#examples}
Producer-Consumer with Error Handling
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define QUEUE_SIZE 10
#define NUM_ITEMS 50
typedef struct {
int* items;
int front;
int rear;
int count;
int size;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
} queue_t;
typedef struct {
int id;
queue_t* queue;
int items_to_process;
} worker_t;
// Initialize queue
int queue_init(queue_t* q, int size) {
q->items = malloc(size * sizeof(int));
if (!q->items) {
return -1;
}
q->front = 0;
q->rear = 0;
q->count = 0;
q->size = size;
if (pthread_mutex_init(&q->mutex, NULL) != 0) {
free(q->items);
return -1;
}
if (pthread_cond_init(&q->not_full, NULL) != 0) {
pthread_mutex_destroy(&q->mutex);
free(q->items);
return -1;
}
if (pthread_cond_init(&q->not_empty, NULL) != 0) {
pthread_cond_destroy(&q->not_full);
pthread_mutex_destroy(&q->mutex);
free(q->items);
return -1;
}
return 0;
}
// Destroy queue
void queue_destroy(queue_t* q) {
pthread_cond_destroy(&q->not_empty);
pthread_cond_destroy(&q->not_full);
pthread_mutex_destroy(&q->mutex);
free(q->items);
}
// Add item to queue
int queue_put(queue_t* q, int item) {
if (pthread_mutex_lock(&q->mutex) != 0) {
return -1;
}
// Wait while queue is full
while (q->count == q->size) {
if (pthread_cond_wait(&q->not_full, &q->mutex) != 0) {
pthread_mutex_unlock(&q->mutex);
return -1;
}
}
// Add item
q->items[q->rear] = item;
q->rear = (q->rear + 1) % q->size;
q->count++;
// Signal that queue is not empty
pthread_cond_signal(&q->not_empty);
if (pthread_mutex_unlock(&q->mutex) != 0) {
return -1;
}
return 0;
}
// Get item from queue
int queue_get(queue_t* q, int* item) {
if (pthread_mutex_lock(&q->mutex) != 0) {
return -1;
}
// Wait while queue is empty
while (q->count == 0) {
if (pthread_cond_wait(&q->not_empty, &q->mutex) != 0) {
pthread_mutex_unlock(&q->mutex);
return -1;
}
}
// Get item
*item = q->items[q->front];
q->front = (q->front + 1) % q->size;
q->count--;
// Signal that queue is not full
pthread_cond_signal(&q->not_full);
if (pthread_mutex_unlock(&q->mutex) != 0) {
return -1;
}
return 0;
}
void* producer(void* arg) {
worker_t* worker = (worker_t*)arg;
for (int i = 0; i < worker->items_to_process; i++) {
int item = worker->id * 1000 + i;
if (queue_put(worker->queue, item) != 0) {
fprintf(stderr, "Producer %d: Failed to put item %d\n",
worker->id, item);
pthread_exit(NULL);
}
printf("Producer %d produced item %d\n", worker->id, item);
usleep(rand() % 100000); // Random delay 0-100ms
}
printf("Producer %d finished\n", worker->id);
pthread_exit(NULL);
}
void* consumer(void* arg) {
worker_t* worker = (worker_t*)arg;
for (int i = 0; i < worker->items_to_process; i++) {
int item;
if (queue_get(worker->queue, &item) != 0) {
fprintf(stderr, "Consumer %d: Failed to get item\n", worker->id);
pthread_exit(NULL);
}
printf("Consumer %d consumed item %d\n", worker->id, item);
usleep(rand() % 150000); // Random delay 0-150ms
}
printf("Consumer %d finished\n", worker->id);
pthread_exit(NULL);
}
int main() {
queue_t queue;
const int NUM_PRODUCERS = 2;
const int NUM_CONSUMERS = 2;
pthread_t producers[NUM_PRODUCERS];
pthread_t consumers[NUM_CONSUMERS];
worker_t* producer_workers[NUM_PRODUCERS];
worker_t* consumer_workers[NUM_CONSUMERS];
// Initialize queue
if (queue_init(&queue, QUEUE_SIZE) != 0) {
fprintf(stderr, "Failed to initialize queue\n");
exit(1);
}
// Create producer workers
for (int i = 0; i < NUM_PRODUCERS; i++) {
producer_workers[i] = malloc(sizeof(worker_t));
if (!producer_workers[i]) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
producer_workers[i]->id = i;
producer_workers[i]->queue = &queue;
producer_workers[i]->items_to_process = NUM_ITEMS / NUM_PRODUCERS;
if (pthread_create(&producers[i], NULL, producer, producer_workers[i]) != 0) {
fprintf(stderr, "Failed to create producer thread %d\n", i);
exit(1);
}
}
// Create consumer workers
for (int i = 0; i < NUM_CONSUMERS; i++) {
consumer_workers[i] = malloc(sizeof(worker_t));
if (!consumer_workers[i]) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
consumer_workers[i]->id = i;
consumer_workers[i]->queue = &queue;
consumer_workers[i]->items_to_process = NUM_ITEMS / NUM_CONSUMERS;
if (pthread_create(&consumers[i], NULL, consumer, consumer_workers[i]) != 0) {
fprintf(stderr, "Failed to create consumer thread %d\n", i);
exit(1);
}
}
// Wait for all producers
for (int i = 0; i < NUM_PRODUCERS; i++) {
if (pthread_join(producers[i], NULL) != 0) {
fprintf(stderr, "Failed to join producer thread %d\n", i);
}
free(producer_workers[i]);
}
// Wait for all consumers
for (int i = 0; i < NUM_CONSUMERS; i++) {
if (pthread_join(consumers[i], NULL) != 0) {
fprintf(stderr, "Failed to join consumer thread %d\n", i);
}
free(consumer_workers[i]);
}
// Cleanup
queue_destroy(&queue);
printf("Program completed successfully\n");
return 0;
}
Compilation Commands
bash
# Basic compilation
gcc -pthread -o program program.c
# With debugging symbols
gcc -g -pthread -o program program.c
# With all warnings
gcc -Wall -Wextra -pthread -o program program.c
# For production (optimized)
gcc -O2 -pthread -o program program.c
# Link with semaphore library (if needed)
gcc -pthread -lrt -o program program.c
Key Takeaways
1. Always handle errors after pthread function calls
2. Use heap allocation for thread parameters and return values
3. Keep critical sections short to minimize lock contention
4. Always join with joinable threads before program exit
5. Use proper synchronization to avoid race conditions
6. Test thoroughly with tools like Valgrind and GDB
7. Follow consistent lock ordering to prevent deadlocks
8. Free allocated memory in the correct thread context