Multithreading Basics
pthread_create and pthread_join
POSIX threads (pthread) are the most commonly used multithreading API in Unix/Linux systems. The following are the basic methods for creating and managing threads.
Thread Creation
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* thread_function(void* arg) {
int thread_num = *(int*)arg;
printf("Thread %d is running\n", thread_num);
pthread_exit(NULL); // Thread exits normally
}
int main() {
pthread_t thread1, thread2;
int num1 = 1, num2 = 2;
// Create thread 1
if (pthread_create(&thread1, NULL, thread_function, &num1) != 0) {
perror("Failed to create thread1");
exit(EXIT_FAILURE);
}
// Create thread 2
if (pthread_create(&thread2, NULL, thread_function, &num2) != 0) {
perror("Failed to create thread2");
exit(EXIT_FAILURE);
}
// Wait for threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Both threads have finished\n");
return 0;
}
Notes
- Parameter Passing: Parameters passed to the thread function must be addressable, typically using pointers
- Thread ID:
pthread_ttype may have different implementations on different systems - Thread Detachment: Use
pthread_detach()to allow the thread to automatically release resources upon termination
Thread Detachment Example
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
pthread_detach(thread); // Automatically release resources after thread ends
Thread Synchronization Mechanisms
Mutex (Mutual Exclusion Lock)
Mutex is the most basic thread synchronization mechanism used to protect shared resources.
Basic Usage
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* increment_counter(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex); // Lock
shared_counter++;
pthread_mutex_unlock(&mutex); // Unlock
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, increment_counter, NULL);
pthread_create(&thread2, NULL, increment_counter, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Final counter value: %d\n", shared_counter);
return 0;
}
Mutex Attributes
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // Recursive lock
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
Common lock types:
PTHREAD_MUTEX_NORMAL: Normal lockPTHREAD_MUTEX_ERRORCHECK: Error-checking lockPTHREAD_MUTEX_RECURSIVE: Recursive lockPTHREAD_MUTEX_DEFAULT: Default type (usually normal lock)
Semaphore
Semaphores are used to control the number of threads accessing shared resources.
Binary Semaphore (Similar to Mutex)
#include <semaphore.h>
#include <stdio.h>
sem_t sem;
int shared_resource = 0;
void* access_resource(void* arg) {
sem_wait(&sem); // P operation (acquire semaphore)
shared_resource++;
printf("Resource accessed by thread %ld, value: %d\n",
pthread_self(), shared_resource);
sem_post(&sem); // V operation (release semaphore)
return NULL;
}
int main() {
pthread_t thread1, thread2;
sem_init(&sem, 0, 1); // Initial value 1 (binary semaphore)
pthread_create(&thread1, NULL, access_resource, NULL);
pthread_create(&thread2, NULL, access_resource, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
sem_destroy(&sem);
return 0;
}
Counting Semaphore
sem_t sem;
#define MAX_RESOURCES 3
void* use_resource(void* arg) {
sem_wait(&sem); // Acquire resource
printf("Thread %ld acquired resource\n", pthread_self());
// Use resource...
sleep(1);
sem_post(&sem); // Release resource
return NULL;
}
int main() {
pthread_t threads[5];
sem_init(&sem, 0, MAX_RESOURCES); // Allow up to 3 threads to access simultaneously
for (int i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, use_resource, NULL);
}
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&sem);
return 0;
}
Condition Variable
Condition variables are used for wait/notify mechanisms between threads.
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
void* producer(void* arg) {
pthread_mutex_lock(&mutex);
printf("Producer: producing data...\n");
sleep(2); // Simulate production time
ready = 1;
printf("Producer: data ready, signaling consumer\n");
pthread_cond_signal(&cond); // Notify consumer
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (!ready) { // Must use while to prevent spurious wakeups
printf("Consumer: waiting for data...\n");
pthread_cond_wait(&cond, &mutex); // Release lock and wait
}
printf("Consumer: consuming data\n");
ready = 0;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
return 0;
}
Thread Safety
Reentrant Functions
Reentrant functions are those that can be safely called from signal handlers or by multiple threads simultaneously.
Reentrant Function Characteristics
- Does not use static or global variables
- Does not call non-reentrant functions
- Uses local variables or passes data via parameters
Example: Non-reentrant vs Reentrant Function
// Non-reentrant function
int non_reentrant_func() {
static int counter = 0; // Static variable
return ++counter;
}
// Reentrant version
int reentrant_func(int* counter) {
return ++(*counter);
}
Common Non-reentrant Functions
strtok()→ Usestrtok_r()insteadlocaltime()→ Uselocaltime_r()rand()→ Userand_r()
Thread-Local Storage (TLS)
Thread-local storage allows each thread to have its own independent instance of a variable.
Using thread_local (C11 Standard)
#include <threads.h>
#include <stdio.h>
thread_local int thread_counter = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 5; i++) {
thread_counter++;
printf("Thread %ld: counter = %d\n", thrd_current(), thread_counter);
}
return NULL;
}
POSIX Implementation
#include <pthread.h>
#include <stdio.h>
__thread int thread_counter = 0; // GCC extension (non-standard)
void* thread_func(void* arg) {
for (int i = 0; i < 5; i++) {
thread_counter++;
printf("Thread %lu: counter = %d\n", pthread_self(), thread_counter);
}
return NULL;
}
Standard POSIX Method (pthread_key_create)
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_key_t key;
void destructor(void* value) {
printf("Destructor called for value %d\n", *(int*)value);
free(value);
}
void* thread_func(void* arg) {
int* counter = malloc(sizeof(int));
*counter = 0;
pthread_setspecific(key, counter);
for (int i = 0; i < 5; i++) {
(*counter)++;
printf("Thread %lu: counter = %d\n", pthread_self(), *counter);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_key_create(&key, destructor); // Create thread-local storage key
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_key_delete(key); // Destroy key
return 0;
}
Signal Handling
Signal Basic Concepts
Signals are a form of inter-process communication used to notify a process that an event has occurred.
Common Signals
| Signal | Name | Description |
|---|---|---|
| SIGINT | 2 | Interrupt (Ctrl+C) |
| SIGTERM | 15 | Termination request |
| SIGKILL | 9 | Force termination (cannot be caught) |
| SIGSEGV | 11 | Segmentation fault |
| SIGALRM | 14 | Alarm clock |
signal Function
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void signal_handler(int signum) {
printf("Received signal %d\n", signum);
}
int main() {
signal(SIGINT, signal_handler); // Catch Ctrl+C
while (1) {
printf("Working...\n");
sleep(1);
}
return 0;
}
Signal Handling Notes
- Unreliable Signals: Traditional signals (1–31) may be lost
- Reentrancy: Signal handlers should only call async-signal-safe functions
- Async-Signal-Safe Functions:
write,_exit,_Exit,abort,signal, etc.
sigaction Function (Recommended)
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void signal_handler(int signum, siginfo_t* info, void* context) {
printf("Received signal %d from PID %d\n", signum, info->si_pid);
}
int main() {
struct sigaction sa;
sa.sa_flags = SA_SIGINFO; // Use extended signal handling
sa.sa_sigaction = signal_handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
while (1) {
printf("Working...\n");
sleep(1);
}
return 0;
}
sigaction Advantages
- More control options
- Can obtain more signal information
- Supports more reliable signal handling
Process Management
fork Function
fork() creates a new process, returning twice: the parent process returns the child’s PID, the child process returns 0.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// Child process
printf("Child process (PID=%d, PPID=%d)\n", getpid(), getppid());
} else {
// Parent process
printf("Parent process (PID=%d, Child PID=%d)\n", getpid(), pid);
}
return 0;
}
fork Characteristics
- Child process gets a copy of the parent’s memory
- File descriptors are shared (but offsets are independent)
- Memory uses Copy-On-Write
exec Function Family
The exec family of functions replaces the current process image.
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child process executes new program
execl("/bin/ls", "ls", "-l", NULL);
// If execl succeeds, code below won't execute
perror("execl failed");
return 1;
} else {
wait(NULL); // Wait for child to finish
}
return 0;
}
Common exec Functions
execl: Variable argument listexecv: Argument arrayexecle: With environment variablesexecve: With environment variables and argument arrayexeclp: Search executable in PATHexecvp: Search executable in PATH (argument array)
wait/waitpid
Wait for child process to terminate.
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
sleep(2);
printf("Child process exiting\n");
return 42; // Child exit status
} else {
int status;
pid_t wpid = wait(&status);
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
}
if (wpid == -1) {
perror("wait failed");
} else {
printf("Waited for PID %d\n", wpid);
}
}
return 0;
}
Advanced waitpid Usage
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid1 = fork();
if (pid1 == 0) {
sleep(2);
return 10;
}
pid_t pid2 = fork();
if (pid2 == 0) {
sleep(1);
return 20;
}
// Wait for specific child
int status;
pid_t wpid = waitpid(pid2, &status, 0); // Wait for pid2
if (WIFEXITED(status)) {
printf("Child %d exited with status %d\n", wpid, WEXITSTATUS(status));
}
// Non-blocking wait
wpid = waitpid(pid1, &status, WNOHANG);
if (wpid == 0) {
printf("Child %d not exited yet\n", pid1);
}
// Wait for any child
wpid = wait(NULL);
printf("Waited for any child: %d\n", wpid);
return 0;
}
Comprehensive Example: Multithreaded Producer-Consumer Model
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t empty, full;
void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
sem_wait(&empty); // Wait for empty slot
pthread_mutex_lock(&mutex);
buffer[in] = i;
printf("Produced: %d at position %d\n", i, in);
in = (in + 1) % BUFFER_SIZE;
count++;
pthread_mutex_unlock(&mutex);
sem_post(&full); // Increase full slot
sleep(1); // Simulate production time
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
sem_wait(&full); // Wait for full slot
pthread_mutex_lock(&mutex);
int item = buffer[out];
printf("Consumed: %d from position %d\n", item, out);
out = (out + 1) % BUFFER_SIZE;
count--;
pthread_mutex_unlock(&mutex);
sem_post(&empty); // Increase empty slot
sleep(2); // Simulate consumption time
}
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
sem_init(&empty, 0, BUFFER_SIZE); // Initially BUFFER_SIZE empty slots
sem_init(&full, 0, 0); // Initially no full slots
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}
Summary
This article provides a detailed introduction to multithreading and process management techniques in C, including:
- Multithreading Basics: Usage of
pthread_createandpthread_join - Thread Synchronization: Implementation and application of mutex, semaphore, and condition variables
- Thread Safety: Reentrant functions and thread-local storage (TLS) implementation
- Signal Handling: Usage and considerations of
signalandsigaction - Process Management: System calls
fork,exec, andwaitand their use cases
These techniques form the foundation for building efficient and stable concurrent programs. In actual development, appropriate synchronization mechanisms should be chosen based on specific needs, and special attention should be paid to thread safety and signal handling. For complex concurrent scenarios, it is recommended to combine multiple synchronization mechanisms and conduct thorough testing to ensure program correctness.



