Chapter 66: POSIX Threads (Pthreads): Creation and Termination

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Understand the fundamental concepts of multithreading and the role of the POSIX Threads (Pthreads) standard in embedded Linux systems.
  • Implement the core Pthread API functions—pthread_createpthread_join, and pthread_exit—to manage the complete lifecycle of a thread.
  • Write, compile, and execute C programs on a Raspberry Pi 5 that create and manage multiple threads of execution.
  • Pass data to a new thread during creation and retrieve a return value from a thread upon its completion.
  • Debug common errors related to thread creation, compilation, and synchronization.
  • Appreciate the difference between a process and a thread and identify scenarios where multithreading is the appropriate solution.

Introduction

Welcome to the world of concurrent programming, a cornerstone of modern embedded systems. In previous chapters, we explored how the Linux kernel manages multiple processes, each with its own isolated memory space. While this process-based multitasking is powerful, it carries significant overhead. Imagine an embedded web server on a Raspberry Pi 5 that needs to handle multiple client requests simultaneously. Creating a new process for every request would be slow and memory-intensive, quickly exhausting the system’s resources. This is where threads provide a more elegant and efficient solution.

A thread, often called a lightweight process, is the smallest unit of execution that the operating system’s scheduler can manage. Multiple threads can exist within a single process, sharing the same memory space, file descriptors, and other resources. This shared context makes inter-thread communication incredibly fast and efficient, but it also introduces new challenges related to synchronization and data protection, which we will begin to explore here and delve into deeper in subsequent chapters.

This chapter introduces the POSIX Threads (Pthreads) standard, a widely adopted API for creating and managing threads in Unix-like operating systems, including Linux. We will focus on the fundamental lifecycle of a thread: its creation, its execution, and its termination. You will learn how to use the pthread_create() function to spawn a new thread, pthread_exit() to gracefully terminate a thread, and pthread_join() to have one thread wait for another to complete. By the end of this chapter, you will be able to write your first multithreaded C programs, a critical skill for developing responsive, high-performance embedded applications.

Technical Background

From Single-Tasking to Multithreading: A Conceptual Journey

To truly appreciate threads, it is helpful to understand the evolution of computing models. Early operating systems were single-tasking; they could only run one program at a time. To run another, the first had to finish completely. The advent of multitasking operating systems, like the Linux kernel, introduced the concept of the process. A process is an instance of a running program, complete with its own virtual memory space, program counter, and system resources. The kernel’s scheduler rapidly switches between processes, giving each a slice of CPU time, creating the illusion of simultaneous execution.

graph TD
    A(<b>Single-Tasking OS</b><br><i>One Program at a Time</i>) --> B{Advent of<br><b>Multitasking</b>};
    
    subgraph "Process-Based Concurrency"
        B --> C(<b>Process 1</b><br>Isolated Memory);
        B --> D(<b>Process 2</b><br>Isolated Memory);
    end

    C --> E{"Context Switch<br><i>(High Overhead)</i>"};
    D --> E;
    
    E --> F("<b>Challenge:</b><br>Slow Inter-Process<br>Communication (IPC)");

    F --> G{A More Efficient<br><b>Solution</b>};

    subgraph "Thread-Based Concurrency (within a single Process)"
       G --> H(<b>Thread A</b><br>Own Stack/PC);
       G --> I(<b>Thread B</b><br>Own Stack/PC);
    end

    H --> J("<b>Shared Memory & Resources</b><br><i>(Fast Communication)</i>");
    I --> J;

    J --> K(<b>Benefit:</b><br>Faster Creation &<br>Context Switching);

    classDef start fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff;
    classDef kernel fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff;
    classDef endo fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef warning fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937;

    class A,B,G start;
    class C,D,H,I kernel;
    class E,F,J,K process;

This process-based model, however, has inherent costs. Each time the system performs a context switch between processes, it must save the state of the current process and load the state of the next. This is a relatively expensive operation. Furthermore, because processes have separate memory spaces by default, sharing information between them requires complex Inter-Process Communication (IPC) mechanisms like pipes, sockets, or shared memory segments.

Threads offer a solution to these challenges. Think of a process as a workshop. The workshop itself has a set of tools (file descriptors), a workbench (memory), and a single blueprint (the program code). In a single-threaded process, there is only one worker. This worker follows the blueprint from start to finish. If the worker has to wait for a delivery (e.g., I/O from a sensor), the entire workshop grinds to a halt.

Now, imagine hiring multiple workers for the same workshop. These are your threads. All workers share the same tools and workbench. They can collaborate on the same blueprint, each working on a different task. One worker might be assembling a component, while another is fetching parts, and a third is reading the next step of the blueprint. Because they share the same space, they can communicate and coordinate with ease—one worker can simply hand a tool to another. This is the essence of multithreading. All threads within a process share the same code, data, and heap segments, as well as resources like open files. However, each thread gets its own stack (for local variables and function calls) and its own program counter, allowing it to execute independently.

The efficiency gain is twofold. First, creating a thread is much faster than creating a process because the operating system doesn’t need to duplicate the entire memory space. Second, a context switch between threads of the same process is significantly faster than a switch between processes, as the memory mapping remains the same. This makes threads ideal for tasks that require high concurrency, such as a server handling multiple clients, a GUI application keeping the interface responsive while performing background calculations, or an embedded system monitoring multiple sensors simultaneously.

The POSIX Standard and Pthreads

In the early days of multithreaded programming, different Unix vendors implemented their own proprietary threading libraries. This made writing portable multithreaded applications a nightmare. To solve this, the Institute of Electrical and Electronics Engineers (IEEE) developed a standard interface for thread management as part of the Portable Operating System Interface (POSIX) standard, specifically IEEE Std 1003.1c-1995. This standard is what we know as Pthreads.

By adhering to the Pthreads API, developers can write multithreaded code that is portable across a vast range of Unix-like systems, from massive servers to embedded Linux devices like the Raspberry Pi.

The Pthreads library, typically libpthread, provides a rich set of functions for all aspects of thread management, including creation, termination, synchronization (using mutexes and condition variables), and scheduling. In this chapter, we will focus on the three functions that form the foundation of the thread lifecycle.

The Thread Lifecycle: Birth, Life, and Death

Every Pthread goes through a distinct lifecycle, managed by a few core functions. Understanding this lifecycle is critical to writing correct and robust multithreaded programs.

1. Thread Creation: pthread_create()

The journey of a thread begins with a call to pthread_create(). This function is the “birth” event for a new thread. It instructs the operating system to create a new thread of execution within the calling process’s context.

The function prototype is defined in <pthread.h> as follows:

C
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

Let’s break down this signature piece by piece, as each argument is crucial:

  • pthread_t *thread: This is a pointer to a variable of type pthread_t. If the function call is successful, this variable will be filled with a unique identifier for the newly created thread. You can think of this as the thread’s “ID card.” The main thread uses this identifier to interact with the new thread later, for instance, to wait for it to finish.
  • const pthread_attr_t *attr: This argument points to a thread attributes object. This object can be used to specify detailed characteristics for the new thread, such as its stack size, scheduling policy, or its “detached” state. If you pass NULL for this argument, the thread is created with default attributes, which is sufficient for most common use cases and what we will use in this chapter. Customizing attributes is an advanced topic we will explore later.
  • void *(*start_routine) (void *): This is the most complex part of the signature, but it represents a simple idea: the work you want the new thread to do. It is a pointer to a function, the thread routine, which the new thread will begin executing as soon as it’s created. This function must have a specific signature: it must accept a single argument of type void * (a generic pointer) and return a value of type void *. This generic pointer mechanism is how you can pass any kind of data to your thread and get any kind of data back.
  • void *arg: This is the argument that will be passed to the start_routine. Whatever value you provide here will become the sole parameter to your thread’s main function. If you need to pass multiple pieces of data, you must bundle them into a struct and pass a pointer to that struct. If the thread routine doesn’t need any input, you can simply pass NULL.

A successful call to pthread_create() returns 0. A non-zero return value indicates an error, such as exceeding the system’s limit on the number of threads. It’s essential to always check the return value of pthread_create().

Once pthread_create() returns successfully, you have two threads of execution running concurrently within your process: the original thread (often called the main thread) and the newly created thread (the child thread). The scheduler is now free to switch between them at any time.

2. Thread Termination: pthread_exit() and return

A thread’s life ends when its start_routine finishes. There are two primary ways this can happen.

The first and most explicit way is by calling pthread_exit() from within the thread routine itself.

C
void pthread_exit(void *retval);

This function terminates the calling thread and makes a return value available to any other thread that joins with the terminated thread. The retval argument is a void * that allows the thread to pass a final status or result back to the main thread. A crucial point to remember is that calling pthread_exit() only terminates the calling thread, not the entire process. If the main thread calls pthread_exit(), all other threads continue to execute. The process itself will only terminate when the last thread terminates.

The second way a thread can terminate is by simply returning from its start_routine. The value returned by the function is treated exactly as if it were passed to pthread_exit(). For example, the following two statements are functionally equivalent within a thread routine:

C
// Option 1: Explicitly exit
pthread_exit(my_result);

// Option 2: Implicitly exit by returning
return my_result;

graph TD
    subgraph "Thread Execution"
        A(Thread is Running...)
    end

    A --> B{How does the thread stop?};
    
    subgraph "Graceful & Correct Termination"
        B -- "Returns from start_routine" --> C(<b>return my_result;</b>);
        B -- "Explicitly calls pthread_exit" --> D("<b>pthread_exit(my_result);</b>");
        C --> E["<b style='color:white'>Clean Termination</b><br>Return value is available to pthread_join()"];
        D --> E;
    end

    subgraph "Dangerous & Incorrect Termination"
        B -- "main() function calls exit()" --> F("<b>exit(0) in main;</b>");
        F --> G[<b style='color:white'>ABRUPT PROCESS TERMINATION</b><br>All threads are killed immediately.<br>No cleanup occurs!];
    end

    classDef running fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef good_path fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef bad_path fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937;
    classDef danger fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff;

    class A running;
    class B decision;
    class C,D good_path;
    class E success;
    class F bad_path;
    class G danger;

Using a return statement is often cleaner and more natural, as it aligns with standard C function semantics.

Warning: A common and dangerous mistake is for the main() function of a program to exit before its child threads have completed their work. If main() exits (either by calling exit() or by returning), the entire process is terminated immediately, and all other threads are killed without any chance for cleanup. This is why the pthread_join() function is so important.

3. Waiting for Threads: pthread_join()

The pthread_join() function is the mechanism for synchronization between threads. It allows one thread to wait for another to finish its execution. It’s analogous to a project manager waiting for a worker to report back that their task is complete before moving on to the next phase.

The function prototype is:

C
int pthread_join(pthread_t thread, void **retval);

Let’s examine its arguments:

  • pthread_t thread: This is the identifier of the thread you want to wait for. It must be the pthread_t variable that was filled by a previous call to pthread_create().
  • void **retval: This is a “pointer to a pointer.” It’s used to retrieve the exit status of the terminated thread. If the target thread terminated by calling pthread_exit(value) or by returning value, then value will be stored at the location pointed to by retval. If you are not interested in the thread’s return value, you can simply pass NULL for this argument.

When a thread calls pthread_join(), it will block (i.e., pause its execution) until the specified target thread terminates. Once the target thread finishes, pthread_join() will “reap” it, freeing any system resources associated with it, and then it will return.

sequenceDiagram
    actor MainThread
    actor NewThread

    par
        MainThread->>+NewThread: pthread_create(start_routine)
        Note over MainThread, NewThread: New thread begins execution concurrently
    end

    MainThread->>MainThread: Continue Execution...
    
    MainThread->>NewThread: pthread_join(new_thread_id, &retval)
    Note over MainThread: Main thread BLOCKS,<br>waiting for New Thread to finish.

    NewThread->>NewThread: Perform work...
    NewThread-->>-MainThread: pthread_exit(result) or return
    Note over NewThread: Thread terminates,<br>returns result.
    
    Note over MainThread: Main thread UNBLOCKS,<br>receives result, and continues.
    MainThread->>MainThread: Process retval, continue execution...

    

Joining is essential for two reasons. First, it ensures that the main thread doesn’t exit prematurely, as discussed earlier. By joining with all the threads it creates, the main thread guarantees that all work is finished before the program terminates. Second, it is the only reliable way to know that a thread has completed its task and to safely retrieve any data it produced. Failing to join with a thread can lead to a zombie thread, where the thread has terminated but its resources have not been released by the system because no other thread has acknowledged its completion. While not as resource-intensive as a zombie process, it’s still a resource leak that should be avoided in robust applications.

Practical Examples

Now, let’s translate this theory into practice on our Raspberry Pi 5. These examples will guide you through writing, compiling, and running basic multithreaded programs.

graph TD
    subgraph "Development Phase"
        A["Write C Code<br><i>(e.g., my_app.c)</i>"] --> B{Compile with GCC};
    end

    subgraph "Compilation & Linking"
        B --> C{"gcc <b>-pthread</b> my_app.c -o my_app"};
    end

    C --> D{Compilation<br>Successful?};
    D -- No --> E["<b style='color:#1f2937'>Fix <i>undefined reference</i><br>or other compiler errors</b>"];
    E --> A;
    
    D -- Yes --> F["Executable Created<br><i>(my_app)</i>"];

    subgraph "Execution Phase"
        F --> G{Run Executable<br><i>./my_app</i>};
        G --> H[Main thread starts];
        H --> I["Main calls <b>pthread_create()</b>"];
        I --> J(New thread starts<br>executing start_routine);
        H --> K[Main thread continues...];
        K --> L{"Main calls <b>pthread_join()</b>"};
        L --> M[Main thread blocks & waits];
        J --> N("New thread finishes<br>and calls <b>pthread_exit()</b>");
        N --> M;
        M --> O[Main thread unblocks];
        O --> P[Program continues...];
        P --> Q(Program Ends);
    end

    classDef start fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff;
    classDef kernel fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff;
    classDef endo fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef warning fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937;

    class A,G,H start;
    class B,C,I,J,K,L,N,O,P process;
    class D decision;
    class E check;
    class M warning;
    class F,Q endo;

Setting Up the Environment

Your Raspberry Pi 5 running Raspberry Pi OS (or any other standard Linux distribution) already has everything you need. The GCC compiler and the Pthreads library are installed by default. You can verify this by opening a terminal. The only special consideration when compiling Pthread code is to link against the Pthread library. This is done by adding the -pthread flag to your gcc command line. This flag tells the compiler to include the necessary header files and link with the libpthread library.

Tip: The -pthread flag should be used instead of the older -lpthread. The -pthread flag ensures that the compiler also defines necessary preprocessor macros for creating thread-safe code.

Example 1: A Simple “Hello World” Thread

Our first program will be the multithreaded equivalent of “hello world.” The main thread will create a new thread, and the new thread will print a message to the console. The main thread will then wait for the new thread to finish before printing its own message and exiting.

File: simple_thread.c

C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // For sleep()

// This is the function that the new thread will execute.
void *thread_function(void *arg) {
    printf("Hello from the new thread! I will work for 2 seconds.\n");
    sleep(2); // Simulate some work
    printf("New thread finishing its work.\n");
    return NULL; // This thread doesn't return any data.
}

int main() {
    pthread_t new_thread_id; // Variable to store the thread ID.
    int ret;

    printf("Main thread: Starting the program.\n");

    // Create a new thread that will execute thread_function
    ret = pthread_create(&new_thread_id, NULL, thread_function, NULL);
    if (ret != 0) {
        // Always check the return value of pthread_create
        fprintf(stderr, "Error: pthread_create() failed with code %d\n", ret);
        return EXIT_FAILURE;
    }

    printf("Main thread: Created a new thread with ID %lu.\n", new_thread_id);
    printf("Main thread: Now waiting for the new thread to finish...\n");

    // Wait for the created thread to terminate.
    // We are not interested in the return value, so we pass NULL.
    ret = pthread_join(new_thread_id, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error: pthread_join() failed with code %d\n", ret);
        return EXIT_FAILURE;
    }

    printf("Main thread: The new thread has finished. Program will now exit.\n");

    return EXIT_SUCCESS;
}

Build and Run Steps:

  1. Save the code: Save the code above into a file named simple_thread.c.
  2. Compile the code: Open a terminal on your Raspberry Pi and compile the program using gcc. Remember the -pthread flag.
    gcc -o simple_thread simple_thread.c -pthread
  3. Run the executable:
    ./simple_thread

Expected Output:

The exact order of the first few lines might vary slightly due to the scheduler, but the overall sequence will be consistent.

Plaintext
Main thread: Starting the program.
Main thread: Created a new thread with ID 140123456789012.
Main thread: Now waiting for the new thread to finish...
Hello from the new thread! I will work for 2 seconds.
New thread finishing its work.
Main thread: The new thread has finished. Program will now exit.

Code Explanation:

  • #include <pthread.h>: This header file contains the declarations for all Pthread functions and data types.
  • thread_function: This is our start routine. It takes a void * argument (which we ignore in this case by not naming it) and returns void *. It prints a message, simulates work with sleep(2), and then returns NULL.
  • main(): The main function acts as the main thread.
  • pthread_t new_thread_id;: We declare a variable to hold the ID of the thread we are about to create.
  • pthread_create(...): This is the core call. We pass the address of our ID variable (&new_thread_id), NULL for default attributes, the name of our start routine (thread_function), and NULL for the argument since our function doesn’t need one. We diligently check the return value ret for errors.
  • pthread_join(...): After creating the thread, the main thread immediately calls pthread_join(). This causes the main thread to block and wait. It will not proceed past this line until thread_function has completed. We pass the ID of the thread we want to wait for and NULL for the return value placeholder because we don’t expect any data back.

Example 2: Passing Arguments and Returning a Value

This next example demonstrates a more realistic scenario: passing data to a thread and getting a result back. We will create a thread that calculates the sum of the first N integers, where N is provided by the main thread. The result of the calculation will be returned to the main thread.

File: thread_sum.c

C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// A structure to hold the result from our thread.
// We use a struct to make it clear what we're returning.
// Note: We will allocate this on the heap.
typedef struct {
    long long sum;
} thread_result_t;

// This is the function that the new thread will execute.
void *sum_runner(void *arg) {
    // The argument is a void pointer, so we must cast it back
    // to the type we expect (a pointer to a long long).
    long long *limit_ptr = (long long *)arg;
    long long limit = *limit_ptr;
    long long sum = 0;

    printf("Thread: Calculating sum from 1 to %lld\n", limit);

    for (long long i = 1; i <= limit; i++) {
        sum += i;
    }

    // Allocate memory on the heap for the result.
    // We cannot return a pointer to a local variable on the thread's stack,
    // as the stack will be destroyed when the thread exits.
    thread_result_t *result = malloc(sizeof(thread_result_t));
    if (result == NULL) {
        fprintf(stderr, "Thread: Failed to allocate memory for result.\n");
        pthread_exit(NULL); // Exit without a result
    }
    
    result->sum = sum;

    // Exit the thread, returning a pointer to the heap-allocated result.
    pthread_exit(result);
}

int main() {
    pthread_t summer_thread_id;
    long long limit = 100000; // The number to sum up to.
    thread_result_t *result_from_thread; // Pointer to hold the result.
    int ret;

    printf("Main: Creating a thread to sum numbers up to %lld\n", limit);

    // Create the thread, passing the address of 'limit' as the argument.
    ret = pthread_create(&summer_thread_id, NULL, sum_runner, &limit);
    if (ret != 0) {
        fprintf(stderr, "Error: pthread_create() failed with code %d\n", ret);
        return EXIT_FAILURE;
    }

    printf("Main: Waiting for the summer thread to finish...\n");

    // Join the thread and capture its return value.
    // The second argument is a pointer to our result pointer.
    ret = pthread_join(summer_thread_id, (void**)&result_from_thread);
    if (ret != 0) {
        fprintf(stderr, "Error: pthread_join() failed with code %d\n", ret);
        return EXIT_FAILURE;
    }

    if (result_from_thread != NULL) {
        printf("Main: The thread finished. The calculated sum is %lld\n", result_from_thread->sum);
        // It is the main thread's responsibility to free the memory
        // that the child thread allocated.
        free(result_from_thread);
    } else {
        printf("Main: The thread did not return a result.\n");
    }
    
    return EXIT_SUCCESS;
}

Build and Run Steps:

  1. Save the code: Save it as thread_sum.c.
  2. Compile:
    gcc -o thread_sum thread_sum.c -pthread
  3. Run:
    ./thread_sum

Expected Output:

Plaintext
Main: Creating a thread to sum numbers up to 100000
Main: Waiting for the summer thread to finish...
Thread: Calculating sum from 1 to 100000
Main: The thread finished. The calculated sum is 5000500000

Code Explanation:

  • Passing the Argument: In main, we call pthread_create with &limit. We are passing the memory address of the limit variable.
  • Receiving the Argument: In sum_runner, the arg parameter receives this address. We cast it from void * to long long * and then dereference it (*limit_ptr) to get the actual value.
  • Returning the Value: The most critical concept here is memory management. The sum_runner function calculates the sum and stores it in a thread_result_t struct. It must allocate memory for this struct on the heap using malloc(). If it tried to return a pointer to a local variable (e.g., thread_result_t result; ... return &result;), that pointer would be invalid the moment the thread exited, as the thread’s stack would be deallocated. This is a classic and severe bug.
  • pthread_exit(result): The thread exits, passing the pointer to the heap-allocated struct as its return value.
  • Receiving the Return Value: The main thread calls pthread_join(). The second argument, (void**)&result_from_thread, is a pointer to our result_from_thread pointer. pthread_join will fill result_from_thread with the address that the child thread returned.
  • Cleanup: After successfully retrieving the result, the main thread now “owns” the memory allocated by the child thread. It is responsible for freeing this memory using free(result_from_thread) to prevent a memory leak.

Common Mistakes & Troubleshooting

When first working with Pthreads, developers often encounter a few common pitfalls. Understanding these ahead of time can save hours of debugging.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Forgetting to Link Pthread Library Compiler error: undefined reference to 'pthread_create' (and others). Always compile with the -pthread flag. Correct command: gcc my_app.c -o my_app -pthread.
Returning a Pointer to a Thread’s Local Variable Main thread reads garbage data or gets a Segmentation fault after a successful pthread_join. The thread must return a pointer to heap-allocated memory (malloc). The joining thread is then responsible for calling free() on that memory.
The Main Thread Exits Prematurely Program finishes instantly. Output from child threads is missing or cut off. The main thread MUST call pthread_join() for every created thread to wait for its completion before exiting main().
Passing Invalid Pointer as Argument Thread receives garbage data as its argument, or the program crashes. Often happens when passing a pointer to a loop variable that has already changed. Ensure the data pointed to is valid for the thread’s lifetime. Use an array of arguments or heap-allocated data instead of a single, changing stack variable.
Not Checking Function Return Codes Program fails silently or behaves erratically under stress (e.g., when creating many threads). Always check the integer return value from pthread_create() and pthread_join(). A non-zero value indicates an error that should be handled.

Exercises

These exercises are designed to be completed on your Raspberry Pi 5. They will solidify your understanding of the thread lifecycle.

  1. Multiple “Hello” Threads:
    • Objective: Modify the first example (simple_thread.c) to create five threads instead of one.
    • Guidance: Use a for loop to create the threads. You will need an array of pthread_t variables to store the IDs. After the creation loop, use another for loop to pthread_join() all five threads. Each thread should print its own “hello” message.
    • Verification: The program should print five “hello” messages from the threads and then the final “exit” message from main. Observe how the scheduler might interleave the output from the different threads.
  2. Passing Unique Data to Each Thread:
    • Objective: Building on the previous exercise, pass a unique integer ID (from 0 to 4) to each of the five threads. Each thread should then print “Hello from thread #ID.”
    • Guidance: You’ll need an array of integers (e.g., int thread_ids[5];). In your creation loop, you’ll pass the address of each element (&thread_ids[i]) to pthread_create. The thread routine will cast the void* argument back to an int* to retrieve its unique ID.
    • Warning: Be careful of a classic bug! If you pass the address of your loop counter variable (&i), all threads might see the final value of i due to a race condition. Using a separate, persistent array for the IDs avoids this problem.
  3. Aggregating Results from Multiple Threads:
    • Objective: Write a program that finds the maximum value in a large array of random numbers using multiple threads.
    • Guidance:
      1. In main, create a large array (e.g., 1 million integers) and fill it with random numbers.
      2. Divide the array into chunks, one for each thread you will create (e.g., 4 threads, each processing 250,000 numbers).
      3. Create a struct to pass information to each thread: a pointer to the start of its chunk and the size of the chunk.
      4. Each thread will find the maximum value within its assigned chunk and return this value (using a heap-allocated integer).
      5. The main thread will pthread_join() each thread, collect the partial maximums, and then find the overall maximum among them.
    • Verification: Run the program and verify its result against a single-threaded version that simply iterates through the whole array.
  4. Timed Waits and Thread Cancellation (Self-Study):
    • Objective: Investigate the functions pthread_timedjoin_np() and pthread_cancel().
    • Guidance: Write a program where the main thread creates a worker thread that enters an infinite loop. The main thread should use pthread_timedjoin_np() to wait for a few seconds. If the thread doesn’t finish in time (which it won’t), the main thread should then use pthread_cancel() to forcibly terminate it. Finally, pthread_join() the cancelled thread to clean it up. The return value for a cancelled thread is PTHREAD_CANCELED.
    • Verification: The program should show the main thread timing out, then cancelling the worker, and then exiting cleanly. This demonstrates a method for handling unresponsive threads.
  5. Understanding pthread_exit() in main:
    • Objective: Demonstrate that when main exits with pthread_exit(), the other threads continue to run.
    • Guidance: Create a program where main spawns a worker thread that sleeps for 5 seconds and then prints a message. Immediately after creating the thread, have main print “Main thread is exiting with pthread_exit()” and then call pthread_exit(NULL). Do not use pthread_join.
    • Verification: You should see the main thread’s exit message, but the program will not terminate. After 5 seconds, the worker thread’s message will appear, and only then will the entire process end. This confirms that the process lives as long as any of its threads are running.

Summary

  • Threads vs. Processes: Threads are lightweight units of execution within a single process that share the same memory space, making them efficient for concurrent tasks but requiring careful synchronization.
  • Pthreads API: The POSIX Threads standard provides a portable C API for managing threads on Linux and other Unix-like systems.
  • pthread_create(): This function spawns a new thread. It requires a pointer to the thread’s start routine and can be used to pass a single void* argument to it.
  • pthread_join(): This function causes the calling thread to block until a target thread terminates. It is essential for ensuring all work is completed and for retrieving a return value from the finished thread.
  • Thread Termination: A thread terminates by returning from its start routine or by explicitly calling pthread_exit().
  • Data Passing: Data is passed to a thread via a pointer in the pthread_create() call. Data is returned from a thread via pthread_join(), but the returned data must be stored on the heap (malloc) to outlive the thread’s stack.
  • Compilation: Pthread programs must be compiled and linked using the -pthread flag with gcc.

Further Reading

  1. Pthreads Manual Pages: The Linux man pages are the definitive source. Access them from your terminal: man pthread_createman pthread_joinman pthread_exit.
  2. POSIX.1-2017 Standard: The official standard from The Open Group. The section on Threads (<pthread.h>) is the authoritative reference. https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/pthread.h.html
  3. “Advanced Programming in the UNIX Environment” by W. Richard Stevens and Stephen A. Rago: Chapter 11 and 12 provide a deep, professional-level dive into thread concepts and the Pthreads API.
  4. “The Linux Programming Interface” by Michael Kerrisk: Chapters 29 through 33 offer an exhaustive and clear explanation of Pthreads, from basics to advanced synchronization primitives.
  5. “Pthreads Programming” – A tutorial from Lawrence Livermore National Laboratory: A well-regarded, concise tutorial covering the essentials of Pthreads programming. https://hpc-tutorials.llnl.gov/posix/
  6. Raspberry Pi Documentation: While not specific to Pthreads, the official hardware documentation is essential for any low-level embedded work on the platform. https://www.raspberrypi.com/documentation/
  7. Eli Bendersky’s Blog: A fantastic resource for deep-dive articles on C, C++, and systems programming. Search for his articles on Pthreads and concurrency for clear, practical explanations.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top