C <threads.h> (C11)
Standard threading support for concurrent programming
๐งต What is <threads.h>?
The threads.h header provides standard threading support in C11, enabling concurrent programming with threads, mutexes, and condition variables. It offers a portable way to create multi-threaded applications without relying on platform-specific libraries like pthreads.
#include <stdio.h>
#include <threads.h>
#include <unistd.h> // for sleep()
// Simple thread function
int worker_thread(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < 3; i++) {
printf("Thread %d: Working... step %d\n", thread_id, i + 1);
thrd_sleep(&(struct timespec){.tv_sec = 1}, NULL); // Sleep 1 second
}
printf("Thread %d: Finished!\n", thread_id);
return thread_id * 10; // Return value
}
int main() {
thrd_t thread1, thread2;
int id1 = 1, id2 = 2;
// Create threads
thrd_create(&thread1, worker_thread, &id1);
thrd_create(&thread2, worker_thread, &id2);
// Wait for threads to complete
int result1, result2;
thrd_join(thread1, &result1);
thrd_join(thread2, &result2);
printf("Main: Thread 1 returned %d, Thread 2 returned %d\n", result1, result2);
return 0;
}
Output:
Thread 1: Working... step 1
Thread 2: Working... step 1
Thread 1: Working... step 2
Thread 2: Working... step 2
Thread 1: Working... step 3
Thread 2: Working... step 3
Thread 1: Finished!
Thread 2: Finished!
Main: Thread 1 returned 10, Thread 2 returned 20
Key Threading Concepts
Threads
Concurrent execution units
thrd_t thread;
thrd_create(&thread, func, arg);
thrd_join(thread, &result);
Mutexes
Mutual exclusion for shared data
mtx_t mutex;
mtx_init(&mutex, mtx_plain);
mtx_lock(&mutex);
mtx_unlock(&mutex);
Condition Variables
Thread synchronization and signaling
cnd_t condition;
cnd_init(&condition);
cnd_wait(&condition, &mutex);
cnd_signal(&condition);
Portability
Standard across platforms
// Works on Windows, Linux, macOS
#include <threads.h>
// No platform-specific code needed
๐น Basic Thread Operations
The C11 <threads.h> standard provides portable functions for creating and managing threads across different platforms. Essential operations include thrd_create() to spawn new threads, thrd_join() to wait for thread completion, thrd_detach() for fire-and-forget threads, and thrd_current() to get the current thread identifier. For example, thrd_create(&thread_id, worker_function, argument) launches a new thread that executes the specified function. These functions abstract platform-specific threading APIs like POSIX threads or Windows threads, enabling developers to write portable concurrent code that compiles and runs correctly on various operating systems without modification.
#include <stdio.h>
#include <threads.h>
#include <stdlib.h>
// Thread function that calculates factorial
int factorial_thread(void* arg) {
int n = *(int*)arg;
int result = 1;
printf("Calculating factorial of %d...\n", n);
for (int i = 1; i <= n; i++) {
result *= i;
printf("Step %d: %d\n", i, result);
thrd_sleep(&(struct timespec){.tv_nsec = 500000000}, NULL); // 0.5 seconds
}
printf("Factorial of %d is %d\n", n, result);
return result;
}
int main() {
thrd_t thread;
int number = 5;
int factorial_result;
printf("Main thread: Starting calculation\n");
// Create thread
if (thrd_create(&thread, factorial_thread, &number) != thrd_success) {
printf("Failed to create thread\n");
return 1;
}
printf("Main thread: Thread created, doing other work...\n");
// Simulate other work in main thread
for (int i = 0; i < 3; i++) {
printf("Main thread: Working on task %d\n", i + 1);
thrd_sleep(&(struct timespec){.tv_sec = 1}, NULL);
}
// Wait for thread to complete
if (thrd_join(thread, &factorial_result) != thrd_success) {
printf("Failed to join thread\n");
return 1;
}
printf("Main thread: Factorial result is %d\n", factorial_result);
return 0;
}
Output:
Main thread: Starting calculation
Main thread: Thread created, doing other work...
Calculating factorial of 5...
Step 1: 1
Main thread: Working on task 1
Step 2: 2
Main thread: Working on task 2
Step 3: 6
Main thread: Working on task 3
Step 4: 24
Step 5: 120
Factorial of 5 is 120
Main thread: Factorial result is 120
๐น Thread Synchronization with Mutexes
Mutexes (mutual exclusion locks) protect shared data from race conditions by ensuring only one thread can access critical sections at a time. The <threads.h> library provides mtx_init() to create mutexes, mtx_lock() to acquire exclusive access, mtx_unlock() to release the lock, and mtx_destroy() for cleanup. For example, wrapping shared variable modifications with mtx_lock(&mutex) and mtx_unlock(&mutex) prevents simultaneous access from multiple threads. Proper mutex usage is crucial for data integrity in multi-threaded applications. Failing to unlock a mutex causes deadlocks, while accessing shared data without locking creates race conditions that lead to unpredictable behavior and difficult-to-reproduce bugs.
#include <stdio.h>
#include <threads.h>
// Shared data
int shared_counter = 0;
mtx_t counter_mutex;
// Thread function that increments counter
int increment_thread(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < 5; i++) {
// Lock mutex before accessing shared data
mtx_lock(&counter_mutex);
int old_value = shared_counter;
thrd_sleep(&(struct timespec){.tv_nsec = 100000000}, NULL); // 0.1 seconds
shared_counter = old_value + 1;
printf("Thread %d: incremented counter from %d to %d\n",
thread_id, old_value, shared_counter);
// Unlock mutex
mtx_unlock(&counter_mutex);
thrd_sleep(&(struct timespec){.tv_nsec = 200000000}, NULL); // 0.2 seconds
}
return 0;
}
int main() {
thrd_t thread1, thread2, thread3;
int id1 = 1, id2 = 2, id3 = 3;
// Initialize mutex
if (mtx_init(&counter_mutex, mtx_plain) != thrd_success) {
printf("Failed to initialize mutex\n");
return 1;
}
printf("Starting threads...\n");
// Create threads
thrd_create(&thread1, increment_thread, &id1);
thrd_create(&thread2, increment_thread, &id2);
thrd_create(&thread3, increment_thread, &id3);
// Wait for all threads
thrd_join(thread1, NULL);
thrd_join(thread2, NULL);
thrd_join(thread3, NULL);
printf("Final counter value: %d\n", shared_counter);
// Cleanup
mtx_destroy(&counter_mutex);
return 0;
}
๐น Producer-Consumer with Condition Variables
Condition variables enable threads to efficiently wait for specific conditions to become true, implementing complex synchronization patterns like producer-consumer queues. Using cnd_init(), cnd_wait(), cnd_signal(), and cnd_broadcast(), threads can sleep until notified rather than busy-waiting. For example, a consumer thread calls cnd_wait(&cond, &mutex) to wait for data availability, while a producer calls cnd_signal(&cond) after adding items. This pattern avoids CPU waste from polling and ensures responsive thread coordination. Condition variables must always be used with mutexes to prevent race conditions between checking conditions and waiting. They're essential for implementing thread pools, message queues, and event-driven architectures in concurrent applications.
#include <stdio.h>
#include <threads.h>
#include <stdlib.h>
#define BUFFER_SIZE 5
// Shared buffer
int buffer[BUFFER_SIZE];
int buffer_count = 0;
int buffer_in = 0;
int buffer_out = 0;
// Synchronization objects
mtx_t buffer_mutex;
cnd_t buffer_not_full;
cnd_t buffer_not_empty;
// Producer thread
int producer(void* arg) {
int producer_id = *(int*)arg;
for (int i = 0; i < 10; i++) {
int item = producer_id * 100 + i;
mtx_lock(&buffer_mutex);
// Wait while buffer is full
while (buffer_count == BUFFER_SIZE) {
printf("Producer %d: Buffer full, waiting...\n", producer_id);
cnd_wait(&buffer_not_full, &buffer_mutex);
}
// Add item to buffer
buffer[buffer_in] = item;
buffer_in = (buffer_in + 1) % BUFFER_SIZE;
buffer_count++;
printf("Producer %d: Produced item %d (buffer count: %d)\n",
producer_id, item, buffer_count);
// Signal that buffer is not empty
cnd_signal(&buffer_not_empty);
mtx_unlock(&buffer_mutex);
thrd_sleep(&(struct timespec){.tv_nsec = 500000000}, NULL); // 0.5 seconds
}
return 0;
}
// Consumer thread
int consumer(void* arg) {
int consumer_id = *(int*)arg;
for (int i = 0; i < 10; i++) {
mtx_lock(&buffer_mutex);
// Wait while buffer is empty
while (buffer_count == 0) {
printf("Consumer %d: Buffer empty, waiting...\n", consumer_id);
cnd_wait(&buffer_not_empty, &buffer_mutex);
}
// Remove item from buffer
int item = buffer[buffer_out];
buffer_out = (buffer_out + 1) % BUFFER_SIZE;
buffer_count--;
printf("Consumer %d: Consumed item %d (buffer count: %d)\n",
consumer_id, item, buffer_count);
// Signal that buffer is not full
cnd_signal(&buffer_not_full);
mtx_unlock(&buffer_mutex);
thrd_sleep(&(struct timespec){.tv_sec = 1}, NULL); // 1 second
}
return 0;
}
int main() {
thrd_t prod_thread, cons_thread;
int prod_id = 1, cons_id = 1;
// Initialize synchronization objects
mtx_init(&buffer_mutex, mtx_plain);
cnd_init(&buffer_not_full);
cnd_init(&buffer_not_empty);
printf("Starting producer-consumer example...\n");
// Create threads
thrd_create(โ_thread, producer, โ_id);
thrd_create(&cons_thread, consumer, &cons_id);
// Wait for threads
thrd_join(prod_thread, NULL);
thrd_join(cons_thread, NULL);
printf("Producer-consumer example completed.\n");
// Cleanup
mtx_destroy(&buffer_mutex);
cnd_destroy(&buffer_not_full);
cnd_destroy(&buffer_not_empty);
return 0;
}
๐น Best Practices and Tips
Following threading best practices ensures reliable, maintainable, and performant multi-threaded applications that avoid common concurrency pitfalls. Always protect shared data with appropriate synchronization mechanisms like mutexes or atomic operations. Minimize time spent holding locks to reduce contention and improve throughput. Avoid nested locks when possible to prevent deadlocks, and if unavoidable, always acquire locks in consistent order across all threads. Use condition variables instead of busy-waiting loops to conserve CPU resources. Initialize all synchronization primitives before thread creation and destroy them only after all threads have finished. Consider using thread-local storage for data that doesn't need sharing. Profile your multi-threaded code to identify bottlenecks and verify correctness with thread sanitizers and stress testing.
โ Best Practices:
- Always check return values: Thread functions can fail
- Use mutexes for shared data: Prevent race conditions
- Join threads: Wait for completion and get return values
- Initialize synchronization objects: Before creating threads
- Destroy resources: Clean up mutexes and condition variables
โ ๏ธ Common Pitfalls:
- Race conditions: Unprotected access to shared data
- Deadlocks: Circular waiting for mutexes
- Resource leaks: Not destroying synchronization objects
- Platform support: Not all systems support <threads.h>
// Good: Error checking and cleanup
int safe_threading_example() {
thrd_t thread;
mtx_t mutex;
// Check initialization
if (mtx_init(&mutex, mtx_plain) != thrd_success) {
return -1;
}
// Check thread creation
if (thrd_create(&thread, worker_func, &data) != thrd_success) {
mtx_destroy(&mutex);
return -1;
}
// Always join threads
int result;
thrd_join(thread, &result);
// Always cleanup
mtx_destroy(&mutex);
return 0;
}