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_create
,pthread_join
, andpthread_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:
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 typepthread_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 passNULL
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 typevoid *
(a generic pointer) and return a value of typevoid *
. 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 thestart_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 astruct
and pass a pointer to thatstruct
. If the thread routine doesn’t need any input, you can simply passNULL
.
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.
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:
// 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. Ifmain()
exits (either by callingexit()
or by returning), the entire process is terminated immediately, and all other threads are killed without any chance for cleanup. This is why thepthread_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:
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 thepthread_t
variable that was filled by a previous call topthread_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 callingpthread_exit(value)
or by returningvalue
, thenvalue
will be stored at the location pointed to byretval
. If you are not interested in the thread’s return value, you can simply passNULL
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
#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:
- Save the code: Save the code above into a file named
simple_thread.c
. - 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
- 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.
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 avoid *
argument (which we ignore in this case by not naming it) and returnsvoid *
. It prints a message, simulates work withsleep(2)
, and then returnsNULL
.main()
: Themain
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
), andNULL
for the argument since our function doesn’t need one. We diligently check the return valueret
for errors.pthread_join(...)
: After creating the thread, the main thread immediately callspthread_join()
. This causes the main thread to block and wait. It will not proceed past this line untilthread_function
has completed. We pass the ID of the thread we want to wait for andNULL
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
#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:
- Save the code: Save it as
thread_sum.c
. - Compile:
gcc -o thread_sum thread_sum.c -pthread
- Run:
./thread_sum
Expected Output:
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 callpthread_create
with&limit
. We are passing the memory address of thelimit
variable. - Receiving the Argument: In
sum_runner
, thearg
parameter receives this address. We cast it fromvoid *
tolong 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 athread_result_t
struct. It must allocate memory for this struct on the heap usingmalloc()
. 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 callspthread_join()
. The second argument,(void**)&result_from_thread
, is a pointer to ourresult_from_thread
pointer.pthread_join
will fillresult_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 usingfree(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.
Exercises
These exercises are designed to be completed on your Raspberry Pi 5. They will solidify your understanding of the thread lifecycle.
- 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 ofpthread_t
variables to store the IDs. After the creation loop, use anotherfor
loop topthread_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.
- Objective: Modify the first example (
- 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]
) topthread_create
. The thread routine will cast thevoid*
argument back to anint*
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 ofi
due to a race condition. Using a separate, persistent array for the IDs avoids this problem.
- 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:
- In
main
, create a large array (e.g., 1 million integers) and fill it with random numbers. - Divide the array into chunks, one for each thread you will create (e.g., 4 threads, each processing 250,000 numbers).
- Create a
struct
to pass information to each thread: a pointer to the start of its chunk and the size of the chunk. - Each thread will find the maximum value within its assigned chunk and return this value (using a heap-allocated integer).
- The main thread will
pthread_join()
each thread, collect the partial maximums, and then find the overall maximum among them.
- In
- Verification: Run the program and verify its result against a single-threaded version that simply iterates through the whole array.
- Timed Waits and Thread Cancellation (Self-Study):
- Objective: Investigate the functions
pthread_timedjoin_np()
andpthread_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 usepthread_cancel()
to forcibly terminate it. Finally,pthread_join()
the cancelled thread to clean it up. The return value for a cancelled thread isPTHREAD_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.
- Objective: Investigate the functions
- Understanding
pthread_exit()
inmain
:- Objective: Demonstrate that when
main
exits withpthread_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, havemain
print “Main thread is exiting with pthread_exit()” and then callpthread_exit(NULL)
. Do not usepthread_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.
- Objective: Demonstrate that when
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 singlevoid*
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 viapthread_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 withgcc
.
Further Reading
- Pthreads Manual Pages: The Linux
man
pages are the definitive source. Access them from your terminal:man pthread_create
,man pthread_join
,man pthread_exit
. - 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 - “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.
- “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.
- “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/
- 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/
- 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.