Thread Basics
Differences Between Threads and Processes
- Process: The basic unit of resource allocation (independent memory space, file handles, etc.).
- Thread: The basic unit of CPU scheduling (shares process memory, lightweight).
- Thread States: Created, ready, running, blocked, terminated.
- Thread ID: Each thread has a unique identifier.
Advantages:
- Shared memory: Thread communication does not require complex IPC (such as pipes or shared memory).
- Low overhead: Creating/switching threads is more efficient than processes.
Introduction to POSIX Threads (pthread)
pthread is the POSIX standard thread library, supported on Linux, macOS, and other systems (Windows requires adaptation via MinGW or Cygwin). Core functions include:
pthread_create: Create a thread.pthread_join: Wait for a thread to terminate.pthread_mutex_*: Mutex operations.pthread_cond_*: Condition variable operations.
Thread Creation and Basic Operations
Thread Creation: pthread_create
pthread_create is used to create a new thread, with the following prototype:
#include <pthread.h>
int pthread_create(
pthread_t *thread, // Thread ID (output parameter)
const pthread_attr_t *attr, // Thread attributes (NULL for default)
void *(*start_routine)(void *), // Thread function (entry point)
void *arg // Argument passed to the thread function
);
Example: Creating a Simple Thread
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // sleep()
// Thread function: print thread ID and argument
void* thread_func(void *arg) {
int thread_id = *(int*)arg;
printf("Thread %d started\n", thread_id);
sleep(1); // Simulate work
printf("Thread %d ended\n", thread_id);
return NULL;
}
int main() {
pthread_t tid1, tid2;
int arg1 = 1, arg2 = 2;
// Create thread 1
if (pthread_create(&tid1, NULL, thread_func, &arg1) != 0) {
perror("pthread_create failed");
return 1;
}
// Create thread 2
if (pthread_create(&tid2, NULL, thread_func, &arg2) != 0) {
perror("pthread_create failed");
return 1;
}
// Wait for threads to finish
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("Main thread ended\n");
return 0;
}
Compilation Command (must link pthread library):
gcc -o thread_demo thread_demo.c -lpthread
Output:
Thread 1 started
Thread 2 started
Thread 1 ended
Thread 2 ended
Main thread ended
Thread Synchronization Mechanisms
When multiple threads concurrently access shared resources, synchronization mechanisms are needed to avoid race conditions. Common synchronization mechanisms include mutexes, condition variables, and semaphores.
Thread Synchronization
- Mutex (
pthread_mutex_t): Used to protect shared resources. - Condition Variable (
pthread_cond_t): Used for inter-thread communication.
Thread Synchronization
Below is an example using a mutex and condition variable to demonstrate how to synchronize access between threads.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *incrementer(void *arg) {
int id = *(int *)arg;
while (1) {
pthread_mutex_lock(&mutex);
if (count >= 10) {
pthread_cond_wait(&cond, &mutex);
} else {
count++;
printf("Thread %d incremented count to %d\n", id, count);
pthread_cond_signal(&cond);
}
pthread_mutex_unlock(&mutex);
usleep(100000); // Sleep 100 milliseconds
}
free(arg);
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
int *id1 = malloc(sizeof(int));
int *id2 = malloc(sizeof(int));
*id1 = 1;
*id2 = 2;
// Create threads
if (pthread_create(&thread1, NULL, incrementer, id1) != 0) {
perror("pthread_create failed");
return 1;
}
if (pthread_create(&thread2, NULL, incrementer, id2) != 0) {
perror("pthread_create failed");
return 1;
}
// Wait for threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// Cleanup
free(id1);
free(id2);
return 0;
}
Mutex
A mutex is used to protect a critical section, ensuring only one thread accesses a shared resource at a time.
Mutex Operation Functions
| Function | Description |
|---|---|
pthread_mutex_init | Initialize mutex |
pthread_mutex_lock | Lock (blocks until lock is acquired) |
pthread_mutex_unlock | Unlock |
pthread_mutex_destroy | Destroy mutex |
Example: Mutex Protecting a Shared Variable
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_NUM 5
int shared_counter = 0; // Shared counter
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // Static mutex initialization
void* increment_counter(void *arg) {
for (int i = 0; i < 1000; i++) {
pthread_mutex_lock(&mutex); // Lock
shared_counter++; // Critical section
pthread_mutex_unlock(&mutex); // Unlock
}
return NULL;
}
int main() {
pthread_t tids[THREAD_NUM];
// Create 5 threads
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&tids[i], NULL, increment_counter, NULL);
}
// Wait for all threads to finish
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(tids[i], NULL);
}
printf("Final counter value: %d (expected %d)\n", shared_counter, THREAD_NUM * 1000);
return 0;
}
Output (when no race condition):
Final counter value: 5000 (expected 5000)
Condition Variable
Condition variables are used for inter-thread communication, allowing threads to wait until a specific condition is met. They are typically used with a mutex.
Condition Variable Operation Functions
| Function | Description |
|---|---|
pthread_cond_init | Initialize condition variable |
pthread_cond_wait | Wait for condition (automatically releases lock, reacquires on wake) |
pthread_cond_signal | Wake one waiting thread |
pthread_cond_broadcast | Wake all waiting threads |
pthread_cond_destroy | Destroy condition variable |
Example: Producer-Consumer Model (Simplified)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0; // Produce/consume positions
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER; // Buffer not full condition
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER; // Buffer not empty condition
// Producer thread: add data to buffer
void* producer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
item = i; // Produce data (example: 0–9)
pthread_mutex_lock(&mutex);
// Wait for buffer not full
while (in == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &mutex);
}
buffer[in++] = item;
printf("Producer %ld produced: %d (buffer position %d)\n", (long)pthread_self(), item, in-1);
pthread_cond_signal(¬_empty); // Wake consumer
pthread_mutex_unlock(&mutex);
usleep(100000); // Simulate production time
}
return NULL;
}
// Consumer thread: remove data from buffer
void* consumer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
// Wait for buffer not empty
while (out == in) {
pthread_cond_wait(¬_empty, &mutex);
}
item = buffer[out++];
printf("Consumer %ld consumed: %d (buffer position %d)\n", (long)pthread_self(), item, out-1);
pthread_cond_signal(¬_full); // Wake producer
pthread_mutex_unlock(&mutex);
usleep(200000); // Simulate consumption time
}
return NULL;
}
int main() {
pthread_t prod_tid, cons_tid;
// Create producer and consumer threads
pthread_create(&prod_tid, NULL, producer, NULL);
pthread_create(&cons_tid, NULL, consumer, NULL);
// Wait for threads to finish
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);
return 0;
}
Key Logic:
- Producer waits for
not_fullcondition (buffer not full) before producing. - Consumer waits for
not_emptycondition (buffer not empty) before consuming. - After producing/consuming, use
pthread_cond_signalto wake the other thread.
Thread Management and Advanced Features
Thread Management
pthread_join: Wait for a thread to terminate.pthread_detach: Detach a thread so it does not need to be joined.
Example Code
Below is a simple multithreading example that demonstrates how to create two threads and have them print messages.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// Thread function
void *print_message(void *arg) {
int id = *(int *)arg;
printf("Hello from thread %d\n", id);
free(arg); // Free passed argument
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
int *id1 = malloc(sizeof(int));
int *id2 = malloc(sizeof(int));
*id1 = 1;
*id2 = 2;
// Create threads
if (pthread_create(&thread1, NULL, print_message, id1) != 0) {
perror("pthread_create failed");
return 1;
}
if (pthread_create(&thread2, NULL, print_message, id2) != 0) {
perror("pthread_create failed");
return 1;
}
// Wait for threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// Cleanup
free(id1);
free(id2);
return 0;
}
Thread Detachment
By default, a thread must be joined using pthread_join after termination. A detached thread automatically releases resources upon termination and does not require joining.
Detached Thread Example
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* detached_thread(void *arg) {
printf("Detached thread started\n");
sleep(1);
printf("Detached thread ended (resources auto-released)\n");
return NULL;
}
int main() {
pthread_t tid;
// Create detached thread (set attribute to PTHREAD_CREATE_DETACHED)
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, detached_thread, NULL) != 0) {
perror("pthread_create failed");
return 1;
}
// No need to join detached thread
sleep(2); // Main thread waits enough time
printf("Main thread ended\n");
return 0;
}
Thread Cancellation
Use pthread_cancel to terminate a thread. It requires cancellation points (e.g., sleep, read) or setting cancellation state.
Thread Cancellation Example
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* cancellable_thread(void *arg) {
printf("Cancellable thread started\n");
while (1) {
printf("Running...\n");
sleep(1); // Cancellation point (sleep is a system call, allows cancellation)
}
return NULL;
}
int main() {
pthread_t tid;
if (pthread_create(&tid, NULL, cancellable_thread, NULL) != 0) {
perror("pthread_create failed");
return 1;
}
sleep(3); // Main thread waits 3 seconds
printf("Attempting to cancel thread\n");
pthread_cancel(tid); // Send cancellation request
// Wait for thread to finish (optional)
void *retval;
pthread_join(tid, &retval);
if (retval == PTHREAD_CANCELED) {
printf("Thread was canceled\n");
}
return 0;
}
Thread Attributes (pthread_attr_t)
Thread attributes are used to set stack size, scheduling policy, priority, etc. Common attributes include:
detachstate: Detach state (PTHREAD_CREATE_JOINABLEorPTHREAD_CREATE_DETACHED).stacksize: Thread stack size (default usually 8MB).
Setting Thread Stack Size
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* large_stack_thread(void *arg) {
char large_array[1024 * 1024]; // 1MB array (test stack size)
printf("Thread stack size test completed\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
size_t stack_size = 2 * 1024 * 1024; // Set stack size to 2MB
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, stack_size); // Set stack size
if (pthread_create(&tid, &attr, large_stack_thread, NULL) != 0) {
perror("pthread_create failed");
return 1;
}
pthread_join(tid, NULL);
pthread_attr_destroy(&attr);
return 0;
}
Thread-Local Storage (TLS)
TLS allows each thread to have its own copy of a variable, suitable for thread-private data (e.g., thread ID, error code).
Using __thread Keyword (GCC Extension)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
__thread int thread_local_var = 0; //
void* tls_thread(void *arg) {
int tid = *(int*)arg;
thread_local_var = tid * 10; // Modify own copy
printf("Thread %d's TLS variable value: %d\n", tid, thread_local_var);
return NULL;
}
int main() {
pthread_t tids[2];
int args[2] = {1, 2};
pthread_create(&tids[0], NULL, tls_thread, &args[0]);
pthread_create(&tids[1], NULL, tls_thread, &args[1]);
pthread_join(tids[0], NULL);
pthread_join(tids[1], NULL);
return 0;
}
Output:
Thread 1's TLS variable value: 10
Thread 2's TLS variable value: 20
Thread Scheduling
Thread scheduling determines execution order. Priority and scheduling policy can be controlled via thread attributes.
Example: Setting Thread Scheduling
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
void *scheduled_thread(void *arg) {
int id = *(int *)arg;
int i = 0;
while (1) {
printf("Thread %d is running, iteration %d\n", id, i++);
usleep(100000); // Sleep 100 milliseconds
}
free(arg);
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
pthread_attr_t attr;
int *id1 = malloc(sizeof(int));
int *id2 = malloc(sizeof(int));
*id1 = 1;
*id2 = 2;
// Initialize thread attributes
if (pthread_attr_init(&attr) != 0) {
perror("pthread_attr_init failed");
return 1;
}
// Set scheduling policy
struct sched_param param;
param.sched_priority = 50; // Set priority
if (pthread_attr_setschedpolicy(&attr, SCHED_FIFO) != 0) {
perror("pthread_attr_setschedpolicy failed");
return 1;
}
if (pthread_attr_setschedparam(&attr, ¶m) != 0) {
perror("pthread_attr_setschedparam failed");
return 1;
}
// Create threads
if (pthread_create(&thread1, &attr, scheduled_thread, id1) != 0) {
perror("pthread_create failed");
return 1;
}
if (pthread_create(&thread2, &attr, scheduled_thread, id2) != 0) {
perror("pthread_create failed");
return 1;
}
// Destroy thread attributes
if (pthread_attr_destroy(&attr) != 0) {
perror("pthread_attr_destroy failed");
return 1;
}
// Main thread continues
printf("Main thread continues...\n");
// Cleanup
free(id1);
free(id2);
return 0;
}
Thread Stack
The thread stack is the space used to store local variables and function call information. Stack size can be specified via thread attributes.
Example: Setting Thread Stack Size
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *stack_thread(void *arg) {
int id = *(int *)arg;
int i = 0;
while (1) {
printf("Thread %d is running, iteration %d\n", id, i++);
usleep(100000); // Sleep 100 milliseconds
}
free(arg);
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
pthread_attr_t attr;
int *id1 = malloc(sizeof(int));
int *id2 = malloc(sizeof(int));
*id1 = 1;
*id2 = 2;
// Initialize thread attributes
if (pthread_attr_init(&attr) != 0) {
perror("pthread_attr_init failed");
return 1;
}
// Set thread stack size
size_t stack_size = 2 * 1024 * 1024; // 2MB
if (pthread_attr_setstacksize(&attr, stack_size) != 0) {
perror("pthread_attr_setstacksize failed");
return 1;
}
// Create threads
if (pthread_create(&thread1, &attr, stack_thread, id1) != 0) {
perror("pthread_create failed");
return 1;
}
if (pthread_create(&thread2, &attr, stack_thread, id2) != 0) {
perror("pthread_create failed");
return 1;
}
// Destroy thread attributes
if (pthread_attr_destroy(&attr) != 0) {
perror("pthread_attr_destroy failed");
return 1;
}
// Main thread continues
printf("Main thread continues...\n");
// Cleanup
free(id1);
free(id2);
return 0;
}
Complete Producer-Consumer Model Example
The producer-consumer model is a classic multithreading synchronization problem that coordinates producer and consumer speeds to avoid buffer overflow or idle waiting. Below is a complete implementation:
Model Design
- Buffer: Fixed-size circular queue (avoids false overflow).
- Mutex: Protects buffer read/write operations.
- Condition Variables:
not_full(buffer not full, producer can produce) andnot_empty(buffer not empty, consumer can consume).
Complete Code
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 5
#define PRODUCER_NUM 2
#define CONSUMER_NUM 2
#define ITEM_NUM 10 // Each producer produces 10 items
typedef struct {
int *buffer; // Buffer array
int in; // Producer write position
int out; // Consumer read position
int count; // Current number of items in buffer (optional, for optimization)
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
} Buffer;
// Initialize buffer
void buffer_init(Buffer *buf) {
buf->buffer = (int*)malloc(BUFFER_SIZE * sizeof(int));
buf->in = 0;
buf->out = 0;
buf->count = 0;
pthread_mutex_init(&buf->mutex, NULL);
pthread_cond_init(&buf->not_full, NULL);
pthread_cond_init(&buf->not_empty, NULL);
}
// Destroy buffer
void buffer_destroy(Buffer *buf) {
free(buf->buffer);
pthread_mutex_destroy(&buf->mutex);
pthread_cond_destroy(&buf->not_full);
pthread_cond_destroy(&buf->not_empty);
}
// Producer thread function
void* producer(void *arg) {
Buffer *buf = (Buffer*)arg;
for (int i = 0; i < ITEM_NUM; i++) {
int item = rand() % 100; // Generate random item (0–99)
pthread_mutex_lock(&buf->mutex);
// Wait for buffer not full
while (buf->count == BUFFER_SIZE) {
printf("Producer %ld waiting for buffer (current %d/%d)\n",
(long)pthread_self(), buf->count, BUFFER_SIZE);
pthread_cond_wait(&buf->not_full, &buf->mutex);
}
// Produce item
buf->buffer[buf->in] = item;
buf->in = (buf->in + 1) % BUFFER_SIZE;
buf->count++;
printf("Producer %ld produced: %d (buffer %d/%d)\n",
(long)pthread_self(), item, buf->count, BUFFER_SIZE);
pthread_cond_signal(&buf->not_empty); // Wake consumer
pthread_mutex_unlock(&buf->mutex);
usleep(rand() % 200000); // Random production time (0–200ms)
}
return NULL;
}
// Consumer thread function
void* consumer(void *arg) {
Buffer *buf = (Buffer*)arg;
for (int i = 0; i < ITEM_NUM; i++) {
pthread_mutex_lock(&buf->mutex);
// Wait for buffer not empty
while (buf->count == 0) {
printf("Consumer %ld waiting for buffer (current %d/%d)\n",
(long)pthread_self(), buf->count, BUFFER_SIZE);
pthread_cond_wait(&buf->not_empty, &buf->mutex);
}
// Consume item
int item = buf->buffer[buf->out];
buf->out = (buf->out + 1) % BUFFER_SIZE;
buf->count--;
printf("Consumer %ld consumed: %d (buffer %d/%d)\n",
(long)pthread_self(), item, buf->count, BUFFER_SIZE);
pthread_cond_signal(&buf->not_full); // Wake producer
pthread_mutex_unlock(&buf->mutex);
usleep(rand() % 300000); // Random consumption time (0–300ms)
}
return NULL;
}
int main() {
Buffer buf;
buffer_init(&buf);
pthread_t prod_tids[PRODUCER_NUM];
pthread_t cons_tids[CONSUMER_NUM];
// Create producer threads
for (int i = 0; i < PRODUCER_NUM; i++) {
pthread_create(&prod_tids[i], NULL, producer, &buf);
}
// Create consumer threads
for (int i = 0; i < CONSUMER_NUM; i++) {
pthread_create(&cons_tids[i], NULL, consumer, &buf);
}
// Wait for all threads to finish
for (int i = 0; i < PRODUCER_NUM; i++) {
pthread_join(prod_tids[i], NULL);
}
for (int i = 0; i < CONSUMER_NUM; i++) {
pthread_join(cons_tids[i], NULL);
}
buffer_destroy(&buf);
return 0;
}
Compile and Run:
gcc -o producer_consumer producer_consumer.c -lpthread
./producer_consumer
Output Explanation:
- Producers/consumers coordinate via condition variables to avoid buffer overflow or idle waiting.
- Use
whileloops to check conditions (notif) to prevent spurious wakeups.
Key Summary
- Thread Creation: Use
pthread_create, pay attention to parameter passing and error checking. - Synchronization Mechanisms: Mutex protects critical sections, condition variables coordinate inter-thread dependencies.
- Thread Management: Detached threads reduce resource usage, thread cancellation requires careful resource cleanup.
- Advanced Features: TLS for thread-private data, stack size settings prevent overflow.



