Managing Threads with POSIX Threads (pthreads)
Thread management involves creation, termination, and synchronization. In Linux, the operating system kernel views threads as lightweight processes sharing the same address space. Since the standard system libraries do not provide a dedicated threading interface, developers rely on the POSIX Threads (pthread) library for multi-threaded programming.
Creating Threads with pthread_create
This function is used to spawn a new thread.
pthread_t *thread: A pointer to store the unique thread identifier.const pthread_attr_t *attr: Attributes for the new thread, often set toNULLfor defaults.void *(*start_routine)(void *): The function pointer for the thread's entry point.void *arg: The single argument passed to the thread function.- Returns: 0 on success, an errer code on failure.
Thread Termination Methods
A thread can end its execution in several ways:
- Returning from its main function.
- Calling
pthread_exit(void *retval)explicitly. - Being canceled by another thread via
pthread_cancel(pthread_t thread).
Joining Threads with pthread_join
This function blocks the calling thread until the specified target thread terminates.
pthread_t thread: The identifier of the thread to wait for.void **retval: An output parameter that receives the pointer to the thread's return value. Its type isvoid **because the thread function returnsvoid *.
Detaching Threads with pthread_detach
If a thread's return value is not needed, joining it creates unnecessary overhead. However, a thread must be reclaimed to prevent resource leaks. Detaching a thread informs the system to automatically release its resources upon completion.
A thread can be detached by another thread using its ID, or it can detach itself using pthread_detach(pthread_self()).
Example: Multi-threaded Sum Calculation
#include <iostream>
#include <pthread.h>
#include <unistd.h>
struct WorkUnit {
int lower_bound;
int upper_bound;
const char* task_label;
WorkUnit(int low, int high, const char* label)
: lower_bound(low), upper_bound(high), task_label(label) {}
};
struct ComputationResult {
int total_sum;
int status_code;
ComputationResult(int sum, int code) : total_sum(sum), status_code(code) {}
};
void* compute_range(void* input) {
WorkUnit* unit = static_cast<WorkUnit*>(input);
ComputationResult* outcome = new ComputationResult(0, 0);
for (int num = unit->lower_bound; num <= unit->upper_bound; ++num) {
usleep(1000); // Simulate work
outcome->total_sum += num;
std::cout << unit->task_label << " (PID: " << getpid() << ") adding: " << num << std::endl;
}
delete unit; // Free memory allocated by main thread
return outcome;
}
int main() {
pthread_t worker_id;
WorkUnit* unit = new WorkUnit(0, 50, "WorkerThread");
std::cout << "Main thread (PID: " << getpid() << ") starting new thread" << std::endl;
pthread_create(&worker_id, NULL, compute_range, unit);
void* raw_output;
pthread_join(worker_id, &raw_output);
ComputationResult* final_result = static_cast<ComputationResult*>(raw_output);
std::cout << unit->task_label << " final sum = " << final_result->total_sum << std::endl;
delete final_result;
return 0;
}
Compile with -lpthread. All threads in a process share the same PID but have unique Lightweight Process IDs (LWP).
Thread IDs and Internal Management
Linux does not implement a native "thread" concept in its kernel; it uses the clone() system call to create execution units sharing resources. The pthread library manages these units, creating Thread Control Blocks (TCBs) and other metadata. This library is loaded into a process's shared memory region. The thread ID (pthread_t) is typically an address within this library's data structures.
Thread Stacks
Each thread has its own independent call stack. Variables allocated on a thread's stack are local to that thread.
Stack Independence Example:
#include <iostream>
#include <pthread.h>
#include <vector>
const int THREAD_COUNT = 4;
void* thread_task(void*) {
int local_var = 42;
return reinterpret_cast<void*>(&local_var);
}
int main() {
std::vector<pthread_t> threads;
for (int i = 0; i < THREAD_COUNT; ++i) {
pthread_t id;
pthread_create(&id, NULL, thread_task, NULL);
threads.push_back(id);
}
for (auto& id : threads) {
void* addr;
pthread_join(id, &addr);
std::cout << "Stack address: " << reinterpret_cast<long>(addr) << std::endl;
}
return 0;
}
Stack Visibility: While stacks are separate, its possible for one thread to access another's stack memory if it obtains a pointer, though this is generally unsafe.
Thread-Local Storage
Global variables are shared across all threads of a process. To create a global variable unique to each thread, use the __thread storage class specifier.
#include <iostream>
#include <pthread.h>
#include <vector>
__thread int per_thread_counter = 0;
void* increment_a(void*) {
per_thread_counter += 50;
std::cout << "Address: " << &per_thread_counter << ", Value: " << per_thread_counter << std::endl;
return nullptr;
}
void* increment_b(void*) {
per_thread_counter += 100;
std::cout << "Address: " << &per_thread_counter << ", Value: " << per_thread_counter << std::endl;
return nullptr;
}
int main() {
std::vector<pthread_t> workers;
pthread_t id;
pthread_create(&id, NULL, increment_a, NULL);
workers.push_back(id);
pthread_create(&id, NULL, increment_b, NULL);
workers.push_back(id);
for (auto& worker : workers) {
pthread_join(worker, NULL);
}
return 0;
}