CodeWithAbdessamad

Multithreading C11

Advanced Topics

Multithreading (C11)

Multithreading is a powerful technique that enables concurrent execution of multiple tasks within a single program. In C11, the POSIX threads (pthread) API provides a robust foundation for implementing threads and synchronization. This section dives deep into practical multithreading patterns using C11, with emphasis on real-world applicability and safety. Let’s start with the fundamentals.

Threads Basics

Threads are lightweight, independent units of execution that share memory and resources within a single process. Unlike processes, threads operate within the same address space, enabling faster communication and reduced overhead. In C11, threads are managed via the pthread library, which follows the POSIX standard for thread operations.

Why threads matter:

Threads allow your program to handle multiple operations simultaneously—such as processing user input while downloading data, or running background tasks without freezing the UI. This is critical for responsive applications and high-performance systems.

Creating Threads

The core of thread management in C11 begins with pthread_create. This function launches a new thread that executes a specified function. Here’s the minimal setup:

<code class="language-c">#include <pthread.h>
<p>#include <stdio.h></p>

<p>void<em> thread_function(void</em> arg) {</p>
<p>    printf("Thread started!\n");</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    pthread<em>t thread</em>id;</p>
<p>    int result = pthread<em>create(&thread</em>id, NULL, thread_function, NULL);</p>
<p>    if (result != 0) {</p>
<p>        fprintf(stderr, "Thread creation failed: %d\n", result);</p>
<p>        return 1;</p>
<p>    }</p>
<p>    printf("Main thread continues...\n");</p>
<p>    // Wait for thread to finish (see Joining section)</p>
<p>    return 0;</p>
<p>}</code>

Key points:

  • pthreadt threadid: A unique identifier for the new thread (type pthread_t).
  • NULL: Specifies no attributes (we’ll cover attributes later).
  • thread_function: The thread’s entry point (a function returning void*).
  • NULL: Passes no arguments to the thread function (we’ll handle arguments later).

This example creates a thread that prints “Thread started!” and continues executing the main thread. Note: Without joining the thread (see below), the main thread exits immediately, terminating the child thread too.

Thread Identifiers and Joining

Each thread has a unique identifier (pthreadt). To ensure proper cleanup, we use pthreadjoin to wait for a thread to complete. This prevents resource leaks and ensures all threads finish before the program exits.

Critical workflow:

  1. Create a thread with pthread_create.
  2. Call pthread_join on the thread ID to block until it finishes.

Here’s a complete example with joining:

<code class="language-c">#include <pthread.h>
<p>#include <stdio.h></p>

<p>void<em> thread_function(void</em> arg) {</p>
<p>    int count = 0;</p>
<p>    while (count < 5) {</p>
<p>        printf("Thread: %d\n", count);</p>
<p>        count++;</p>
<p>        // Simulate work with a short delay</p>
<p>        sleep(1);</p>
<p>    }</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    pthread<em>t thread</em>id;</p>
<p>    int result = pthread<em>create(&thread</em>id, NULL, thread_function, NULL);</p>
<p>    if (result != 0) {</p>
<p>        fprintf(stderr, "Thread creation failed: %d\n", result);</p>
<p>        return 1;</p>
<p>    }</p>

<p>    // Wait for thread to finish</p>
<p>    void* result_ptr;</p>
<p>    pthread<em>join(thread</em>id, &result_ptr);</p>
<p>    printf("Main thread joined successfully.\n");</p>
<p>    return 0;</p>
<p>}</code>

Why joining matters:

Without pthreadjoin, the program exits before the child thread completes, causing undefined behavior. The resultptr (here NULL) captures the thread’s return value, which we ignore in this example but use in production for error handling.

Thread Functions and Arguments

Threads can accept arguments via the pthread_create call. This allows passing data to the thread function. For example:

<code class="language-c">void<em> thread_function(void</em> arg) {
<p>    int value = <em>(int</em>)arg;</p>
<p>    printf("Thread received value: %d\n", value);</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    int my_value = 42;</p>
<p>    pthread<em>t thread</em>id;</p>
<p>    pthread<em>create(&thread</em>id, NULL, thread<em>function, &my</em>value);</p>
<p>    pthread<em>join(thread</em>id, NULL);</p>
<p>    return 0;</p>
<p>}</code>

Important: The argument arg is cast to int* to access the integer value. Always handle memory safety—passing large structures or dynamically allocated memory requires careful management (we’ll cover this in advanced patterns).

Synchronization

Without synchronization, concurrent threads can cause race conditions (e.g., corrupted data, deadlocks). C11 provides atomic primitives and synchronization objects to manage shared resources safely.

Why Synchronization Is Needed

Imagine two threads incrementing a shared counter:

<code class="language-c">int counter = 0;

<p>void<em> increment(void</em> arg) {</p>
<p>    for (int i = 0; i < 1000; i++) {</p>
<p>        counter++;</p>
<p>    }</p>
<p>}</code>

Without synchronization, the counter might end at 998 (due to overlapping increments). Synchronization ensures mutual exclusion and ordered execution.

Mutexes (Mutual Exclusion)

Mutexes are the most common synchronization tool. They lock a resource so only one thread can access it at a time.

Example: Protecting a shared counter with a mutex:

<code class="language-c">#include <pthread.h>
<p>#include <stdio.h></p>

<p>pthread<em>mutex</em>t lock;</p>
<p>int counter = 0;</p>

<p>void<em> increment(void</em> arg) {</p>
<p>    for (int i = 0; i < 1000; i++) {</p>
<p>        pthread<em>mutex</em>lock(&lock);</p>
<p>        counter++;</p>
<p>        pthread<em>mutex</em>unlock(&lock);</p>
<p>    }</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    pthread_t thread1, thread2;</p>
<p>    pthread_create(&thread1, NULL, increment, NULL);</p>
<p>    pthread_create(&thread2, NULL, increment, NULL);</p>
<p>    </p>
<p>    pthread_join(thread1, NULL);</p>
<p>    pthread_join(thread2, NULL);</p>
<p>    printf("Final counter: %d\n", counter);</p>
<p>    return 0;</p>
<p>}</code>

Key mechanics:

  • pthreadmutexinit(&lock, NULL): Initialize the mutex (we skip initialization here for brevity; always call this in production).
  • pthreadmutexlock(&lock): Acquire the lock (blocks until available).
  • pthreadmutexunlock(&lock): Release the lock.

Why this works: The mutex ensures only one thread increments counter at a time, preventing race conditions.

Condition Variables

Condition variables enable threads to wait for specific events (e.g., “data is ready”). They work with mutexes to avoid busy-waiting.

Example: A producer-consumer pattern:

<code class="language-c">#include <pthread.h>
<p>#include <stdio.h></p>

<p>pthread<em>mutex</em>t mutex;</p>
<p>pthread<em>cond</em>t cond;</p>
<p>int buffer = 0;</p>
<p>int max = 10;</p>

<p>void<em> producer(void</em> arg) {</p>
<p>    for (int i = 0; i < 10; i++) {</p>
<p>        pthread<em>mutex</em>lock(&mutex);</p>
<p>        while (buffer == max) {</p>
<p>            pthread<em>cond</em>wait(&cond, &mutex);</p>
<p>        }</p>
<p>        buffer++;</p>
<p>        printf("Producer added: %d\n", buffer);</p>
<p>        pthread<em>mutex</em>unlock(&mutex);</p>
<p>        // Simulate work</p>
<p>        sleep(1);</p>
<p>    }</p>
<p>    return NULL;</p>
<p>}</p>

<p>void<em> consumer(void</em> arg) {</p>
<p>    for (int i = 0; i < 10; i++) {</p>
<p>        pthread<em>mutex</em>lock(&mutex);</p>
<p>        while (buffer == 0) {</p>
<p>            pthread<em>cond</em>wait(&cond, &mutex);</p>
<p>        }</p>
<p>        buffer--;</p>
<p>        printf("Consumer removed: %d\n", buffer);</p>
<p>        pthread<em>mutex</em>unlock(&mutex);</p>
<p>        sleep(1);</p>
<p>    }</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    pthread<em>mutex</em>init(&mutex, NULL);</p>
<p>    pthread<em>cond</em>init(&cond, NULL);</p>
<p>    </p>
<p>    pthread_t prod, cons;</p>
<p>    pthread_create(&prod, NULL, producer, NULL);</p>
<p>    pthread_create(&cons, NULL, consumer, NULL);</p>
<p>    </p>
<p>    pthread_join(prod, NULL);</p>
<p>    pthread_join(cons, NULL);</p>
<p>    </p>
<p>    pthread<em>mutex</em>destroy(&mutex);</p>
<p>    pthread<em>cond</em>destroy(&cond);</p>
<p>    return 0;</p>
<p>}</code>

How condition variables work:

  1. Wait: pthreadcondwait blocks the thread until a signal occurs (while holding the mutex).
  2. Signal: Another thread calls pthreadcondsignal to wake one waiting thread.
  3. Broadcast: pthreadcondbroadcast wakes all waiting threads.

Critical note: Always pair condition variables with mutexes—never use them alone.

Semaphores

Semaphores are counters that control access to a resource. They’re simpler than mutexes but less flexible.

Example: A semaphore for thread-safe access to a limited resource:

<code class="language-c">#include <pthread.h>
<p>#include <stdio.h></p>

<p>sem_t sem;</p>

<p>void<em> resource_access(void</em> arg) {</p>
<p>    sem_wait(&sem); // Acquire semaphore</p>
<p>    printf("Thread %d: using resource\n", (int)arg);</p>
<p>    sem_post(&sem); // Release semaphore</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    sem_init(&sem, 0, 3); // Initialize to 3</p>
<p>    </p>
<p>    pthread_t threads[4];</p>
<p>    for (int i = 0; i < 4; i++) {</p>
<p>        pthread<em>create(&threads[i], NULL, resource</em>access, (void*)i);</p>
<p>    }</p>
<p>    </p>
<p>    for (int i = 0; i < 4; i++) {</p>
<p>        pthread_join(threads[i], NULL);</p>
<p>    }</p>
<p>    </p>
<p>    sem_destroy(&sem);</p>
<p>    return 0;</p>
<p>}</code>

Key differences from mutexes:

  • Semaphores can be used for counting (e.g., limiting concurrent threads).
  • Mutexes are for mutual exclusion (only one thread at a time).
Barriers

Barriers synchronize threads to a common point (e.g., “all threads must finish before proceeding”).

Example: A simple barrier with 3 threads:

<code class="language-c">#include <pthread.h>
<p>#include <stdio.h></p>

<p>pthread<em>barrier</em>t barrier;</p>

<p>void<em> barrier_example(void</em> arg) {</p>
<p>    pthread<em>barrier</em>wait(&barrier);</p>
<p>    printf("Thread %d reached barrier\n", (int)arg);</p>
<p>    return NULL;</p>
<p>}</p>

<p>int main() {</p>
<p>    pthread<em>barrier</em>init(&barrier, NULL, 3);</p>
<p>    </p>
<p>    pthread_t threads[3];</p>
<p>    for (int i = 0; i < 3; i++) {</p>
<p>        pthread<em>create(&threads[i], NULL, barrier</em>example, (void*)i);</p>
<p>    }</p>
<p>    </p>
<p>    for (int i = 0; i < 3; i++) {</p>
<p>        pthread_join(threads[i], NULL);</p>
<p>    }</p>
<p>    </p>
<p>    pthread<em>barrier</em>destroy(&barrier);</p>
<p>    return 0;</p>
<p>}</code>

Use case: Ensuring all threads complete a phase before moving to the next (e.g., in parallel processing pipelines).

Summary

Multithreading in C11 unlocks concurrency without sacrificing safety. Threads are the building blocks of parallel execution—created via pthreadcreate and joined via pthreadjoin to ensure clean resource management. Synchronization (mutexes, condition variables, semaphores, and barriers) prevents race conditions and deadlocks, enabling robust concurrent systems. By mastering these patterns, you can build responsive, scalable applications that handle real-world complexity with confidence. 🌟