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;
}

๐Ÿง  Test Your Knowledge

What is the primary purpose of a mutex in threading?