0% found this document useful (0 votes)
7 views27 pages

POSIX Concurrency in C - Complete Guide2

The document is a comprehensive guide on POSIX concurrency in C, covering multithreaded programming basics, thread synchronization, debugging techniques, and best practices. It includes practical examples of thread creation, mutexes, semaphores, and debugging tools like GDB and Valgrind. The guide emphasizes error handling, memory management, and proper thread lifecycle management to ensure efficient and safe multithreaded applications.

Uploaded by

oddball
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views27 pages

POSIX Concurrency in C - Complete Guide2

The document is a comprehensive guide on POSIX concurrency in C, covering multithreaded programming basics, thread synchronization, debugging techniques, and best practices. It includes practical examples of thread creation, mutexes, semaphores, and debugging tools like GDB and Valgrind. The guide emphasizes error handling, memory management, and proper thread lifecycle management to ensure efficient and safe multithreaded applications.

Uploaded by

oddball
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 27

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

You might also like