Module 2 · Unit 4 · Lesson 5 of 5
POSIX Threads
The standard C interface to kernel threads on Linux, macOS, and the BSDs. Creating, joining, detaching, and configuring threads; handling errors; and the pitfalls that trip up programmers new to the pthread programming model.
01 Introduction
The previous four lessons in this unit have been largely conceptual: what threads are, where they can live in the system, how the kernel schedules them, and what happens to a program when several of them run at once. This lesson is the practical counterpart. It introduces the POSIX threads API — commonly called pthreads — which is the standard interface for creating and managing threads in C on any UNIX-like system.
The pthreads standard was formalised in IEEE 1003.1c (1995) and is now part of POSIX.1. It is available on Linux (via glibc's Native POSIX Thread Library, examined in Lesson 4.3), on macOS, on the BSDs, and on Solaris. It is the API you will use for every laboratory and assignment in this course that involves threads. Learning it well is not just a matter of memorising a few function names — the pthread programming model has its own conventions for error handling, resource management, and thread lifecycle, and getting those conventions right is the difference between code that works and code that works reliably.
The lesson covers the five core functions (pthread_create,
pthread_join, pthread_detach,
pthread_exit, and the attribute machinery), the
thread lifecycle from creation to completion, the standard
compilation conventions, and the pitfalls — including a few
that are specific to Linux and glibc. It closes with a complete
working program that illustrates everything discussed, and
prepares you for the thread-management work in Lab 3.
Prerequisites
This lesson builds on Lesson 4.3 (kernel threads — the model pthreads implements on Linux) and Lesson 4.4 (multicore processing — what pthreads enables). It also assumes the development toolchain from Lesson 2.5 is installed, since every example in this lesson compiles and runs in the course VM.
02 Learning objectives
By the end of this lesson you will be able to
- Create threads with
pthread_create. - Join threads with
pthread_join. - Manage thread attributes.
- Handle thread errors.
- Write correct multithreaded code.
This lesson contributes to U4.4, U4.5, and to course learning outcomes CLO2, CLO6, and CLO8.
03 Required reading
- OSTEP — Chapter 27, Interlude: Thread API. A concise introduction to the POSIX threads interface with example code.
- Operating System Concepts — Chapter 4, Section 4.4, Thread Libraries, and the Pthreads examples throughout Chapter 4.
- Butenhof, D. R. — Programming with POSIX Threads, Chapters 1–3. The classical reference for the pthreads programming model. Chapter 1 gives the philosophy, Chapter 2 covers thread creation and termination, and Chapter 3 covers attributes.
-
Linux man pages:
man 7 pthreads,man 3 pthread_create,man 3 pthread_join,man 3 pthread_detach,man 3 pthread_exit,man 3 pthread_attr_init,man 3 pthread_attr_setdetachstate,man 3 pthread_attr_setstacksize,man 3 pthread_self,man 3 pthread_equal.
Reading strategy
Read OSTEP Chapter 27 first for the API surface, then
Butenhof Chapter 2 for the design rationale and the
conventions. Keep the man pages open while you work through
the worked example — the descriptions of edge cases
(particularly around the return values and the semantics of
pthread_join on an already-joined thread) are
more precise than the textbook treatments.
04 Core content
4.1 The pthread API in one page
The pthreads API is larger than it first appears, but the subset you will use in this course is small. Six functions cover almost every case.
| Function | Purpose |
|---|---|
pthread_create |
Create a new thread that runs a specified function. |
pthread_join |
Wait for a thread to terminate and collect its return value. |
pthread_detach |
Mark a thread as detached so its resources are released automatically on exit. |
pthread_exit |
Terminate the calling thread. |
pthread_self |
Return the calling thread's identifier. |
pthread_equal |
Compare two thread identifiers for equality. |
All of them return an integer error code, discussed in
section 4.7. All of them are declared in the header
<pthread.h>. And all of them follow the
conventions that POSIX established for the whole threading
interface.
4.2 Creating a thread: pthread_create
The signature is:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
The four arguments have specific roles.
-
threadis a pointer to apthread_tvariable, which the function fills in with the new thread's identifier. This identifier is what you pass topthread_joinorpthread_detachlater. -
attris a pointer to apthread_attr_tstructure describing thread attributes, orNULLto accept the defaults. The default attributes are almost always what you want; section 4.6 discusses when to override them. -
start_routineis a function pointer to the thread's entry point. The function must have the exact signaturevoid *(*)(void *)— it takes onevoid *argument and returnsvoid *. -
argis the argument passed tostart_routine. If you need to pass multiple values, wrap them in a struct and pass a pointer to it.
The function returns 0 on success or an error number on failure
— not the convention of setting errno and
returning −1 that the rest of POSIX uses. This inconsistency
is important enough that section 4.7 is devoted to it.
A minimal creation call:
pthread_t t;
int err = pthread_create(&t, NULL, worker, NULL);
if (err != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
4.3 The thread start routine
The start routine is an ordinary function, but its signature is fixed by the API. The most common form takes a pointer to a struct that holds the arguments:
struct args {
int id;
double *data;
size_t n;
double result;
};
void *worker(void *p) {
struct args *a = p;
double sum = 0.0;
for (size_t i = 0; i < a->n; i++) sum += a->data[i];
a->result = sum;
return NULL;
}
The void * return value is the thread's exit
value, which pthread_join can retrieve. A thread
that has nothing to return can return NULL (or,
equivalently, call pthread_exit(NULL)).
Returning a pointer to a local variable
A common mistake is to return the address of a local variable from the start routine:
void *worker(void *p) {
int result = 42;
return &result; /* WRONG — the variable is gone on return */
}
The thread's stack is deallocated when the thread terminates,
so the address is invalid by the time the caller sees it. To
return an integer result, either return it cast to
void * (if it fits), or store it in a location
whose lifetime is controlled by the parent — for example, a
field in the argument struct, or a value the parent passes
in via pthread_join's second argument.
4.4 Waiting for threads: pthread_join
Threads are analogous to processes in that a thread may be
joinable or detached. A joinable thread is
one whose resources are not reclaimed by the system until
another thread calls pthread_join on its
identifier. A detached thread's resources are reclaimed
automatically when it terminates, with no join required.
By default, pthread_create creates joinable
threads. The signature of pthread_join is:
int pthread_join(pthread_t thread, void **retval);
The function blocks the calling thread until the target thread
terminates. If retval is not NULL,
the target thread's return value (the pointer it returned from
its start routine, or the argument to pthread_exit)
is stored at *retval. Once a thread has been
joined, its resources are fully reclaimed and its identifier
may be reused.
The essential rule for joinable threads: every thread that is created must eventually be joined or detached. A joinable thread that is never joined is a resource leak — its kernel task structure and stack remain allocated until the process terminates. This is the threading analogue of the zombie-process problem from Unit 3.
A joining pattern for multiple threads:
pthread_t t[N];
for (int i = 0; i < N; i++)
pthread_create(&t[i], NULL, worker, &args[i]);
for (int i = 0; i < N; i++)
pthread_join(t[i], NULL);
Note that the joins are in a separate loop from the creations. Creating all threads first and then joining them lets the threads run in parallel. If the create-then-join were done in one loop, each thread would be created and immediately joined, serialising the whole computation.
4.5 Detached threads: pthread_detach
Not every thread needs to be joined. A fire-and-forget thread — one whose result no one will ever collect — should be detached so that its resources are reclaimed on exit. There are two ways to create a detached thread.
The first is to call pthread_detach after
creation:
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
pthread_detach(t);
The second is to set the detach state in the attributes
structure before calling pthread_create, which
avoids the race between thread creation and the detach call:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,
PTHREAD_CREATE_DETACHED);
pthread_create(&t, &attr, worker, NULL);
pthread_attr_destroy(&attr);
The race matters when the thread might terminate before
pthread_detach is called on its identifier. If
the thread has already terminated, its identifier may no
longer refer to it, and the detach call may fail. Setting the
attribute before creation eliminates the race entirely.
Joinable or detached: a design decision
A joinable thread is appropriate when the parent needs the thread's result or needs to know when it completed. A detached thread is appropriate when the work is fire-and-forget — for example, a background logging task, a telemetry heartbeat, or a request handler whose result is delivered elsewhere. Making this decision explicitly at creation time avoids resource leaks.
4.6 Thread attributes
The pthread_attr_t structure is an opaque type
that describes the initial configuration of a new thread.
Using it has three steps: initialise, set the values you
care about, and destroy it after creating the thread.
pthread_attr_t attr;
pthread_attr_init(&attr);
/* ... pthread_attr_set... calls ... */
pthread_create(&t, &attr, worker, NULL);
pthread_attr_destroy(&attr);
The attributes you will most often set are:
- Detach state
-
PTHREAD_CREATE_JOINABLE(the default) orPTHREAD_CREATE_DETACHED. Set withpthread_attr_setdetachstate. - Stack size
-
The size of the thread's user-space stack. The default on
Linux is typically 8 MB, which is generous for ordinary
threads but wasteful when creating thousands of them. Set
with
pthread_attr_setstacksize. - Stack address
- The base address of the thread's stack. Rarely used; some applications pass a pre-allocated stack so they can control its placement, for example to keep thread stacks within a single NUMA node.
- Scheduling policy and priority
-
SCHED_OTHER,SCHED_FIFO, orSCHED_RRwith an associated priority. Real-time policies require privileges and are discussed in Unit 5. - Guard size
- A region of protected memory placed after the thread's stack so that a stack overflow triggers a fault rather than silently corrupting adjacent memory. Default is one page.
4.7 Error handling and the -pthread flag
The pthreads functions return an integer error code and do not
use errno. This is a deliberate design choice
from POSIX 1003.1c: because errno is a per-thread
variable on a modern system, but was not when pthreads was
standardised, the interface opted to return errors directly
and avoid any shared or thread-local state ambiguity.
The correct pattern is:
int err = pthread_create(&t, NULL, worker, NULL);
if (err != 0) {
fprintf(stderr, "pthread_create failed: %s\n", strerror(err));
return 1;
}
Never use perror with a pthreads error code:
perror reads from errno, which the
pthreads functions do not set. The result is a misleading
message about a completely different error.
The other convention specific to pthreads is the compiler and
linker flag -pthread. On modern glibc this flag
both defines the preprocessor macro _REENTRANT
(which enables thread-safe variants of some library functions)
and links the program against the threading library. Without
-pthread, the program may compile but produce
incorrect behaviour under concurrency, or fail to link at all.
gcc -Wall -Wextra -g -O2 -pthread -o program program.c
On macOS and the BSDs, the same flag works, though modern versions of these systems often provide threading in the base C library. On Linux, the flag is mandatory.
4.8 The thread lifecycle
Bringing the functions together, a thread passes through a sequence of states that mirrors the process lifecycle from Unit 3 but is specific to the pthreads model.
pthread_join
(if joinable) or automatically on termination (if detached).
4.9 Thread-local storage
In Lesson 4.1 we saw that each thread has its own
errno even though errno looks like a
global variable. This is an example of thread-local
storage (TLS): a variable that appears to each thread
to be private, even though it is declared at file scope.
Thread-local storage is useful whenever a thread needs to maintain state across calls to library functions without passing that state explicitly. The POSIX interface has two forms.
The compiler-based form uses the _Thread_local
storage-class specifier (C11; __thread in GCC and
Clang). Each thread sees its own copy of the variable:
_Thread_local int counter = 0;
void bump(void) {
counter++; /* each thread increments its own copy */
}
The POSIX form uses pthread_key_create and
pthread_setspecific to associate a value with a
key that is unique to the calling thread. This is the older,
more portable interface and is what the C library uses
internally for errno:
pthread_key_t key;
void init(void) {
pthread_key_create(&key, NULL);
}
void set(int value) {
pthread_setspecific(key, (void *)(long)value);
}
int get(void) {
return (int)(long)pthread_getspecific(key);
}
TLS is a small but important part of the pthreads programming model. It gives you a way to have per-thread state without passing it through every function call, and it is what makes the C library safe to use from multiple threads at once. We will return to it in Unit 6, when we discuss how synchronization primitives are themselves implemented using per-thread state.
4.10 Common pitfalls
The pthread API is small, but its programming model has a set of pitfalls that beginners reliably encounter. Most of them are consequences of the shared-address-space design from Lesson 4.1.
Forgetting -pthread
The program may compile and link but exhibit incorrect
behaviour under concurrency, because some library
functions are not replaced with their thread-safe
variants. Always compile with -pthread.
Using errno for pthreads errors
Pthreads functions return their error codes directly.
Using perror after a failed
pthread_create reports a stale, unrelated
error. Use strerror(err).
Returning a pointer to a local
The thread's stack is destroyed on exit. Returning a pointer into it produces undefined behaviour when the parent reads through it.
Passing the address of a loop variable
Creating threads in a loop and passing
&i gives every thread the same address,
whose value changes as the loop proceeds. Pass a
per-thread struct instead.
Never joining a joinable thread
A joinable thread that is never joined leaks its kernel task and its stack. Either join it or create it detached.
Sharing data without synchronization
Threads share the heap and globals; unsynchronized access
is a race condition. Even
counter++ is not atomic. The subject of
Unit 6.
Assuming thread scheduling order
The scheduler does not guarantee that threads run in the
order they were created, nor that a thread runs
immediately after pthread_create returns.
Using exit instead of pthread_exit
Calling exit from a thread terminates the
entire process. To terminate only the calling thread, use
pthread_exit or return from the start
routine.
4.11 A note on pthread_cancel
POSIX provides pthread_cancel, which requests
that another thread be cancelled. The mechanism is subtle: a
cancel request is not a command that terminates the target
immediately. The target thread will be terminated only when
it next reaches a cancellation point — one of
a defined set of functions, including read,
write, sleep, wait,
pthread_join, and pthread_cond_wait.
Between cancellation points, the thread continues to run.
Cancellation also has two modes — deferred (the
default) and asynchronous — and the state can be
disabled entirely. Because correct cancellation requires the
target thread to handle a signal-like event and clean up
after itself (using pthread_cleanup_push and
pthread_cleanup_pop), the mechanism is more
complex than it first appears.
In this course, you will not need to use
pthread_cancel. When a thread must be asked to
stop, the conventional pattern is a shared flag that the
thread checks periodically — a form of cooperative
cancellation that is simpler and more predictable than the
POSIX mechanism. We will use this pattern in the threading
laboratory.
05 Worked example
To make the API concrete, walk through a complete program that uses every function discussed. It creates a fixed number of worker threads, each of which computes a partial result; the main thread joins them all and combines the results.
Parallel sum with pthreads
The task is to sum an array of 400 million integers. The array is partitioned across four threads; each thread sums its own slice and writes its partial sum to its own slot in a result array; the main thread joins the four threads and combines the results. The program also prints a per-thread diagnostic that includes each thread's identifier.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
#define N 400000000L
#define WORKERS 4
static long data[N];
static long partial[WORKERS];
struct args {
int id; /* thread index */
size_t start;
size_t end;
};
static void *sum_worker(void *p) {
struct args *a = p;
long s = 0;
for (size_t i = a->start; i < a->end; i++) s += data[i];
partial[a->id] = s;
printf("thread %d: pid=%d, sum=%ld\n",
a->id, (int)getpid(), s);
return NULL;
}
static double now(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
int main(void) {
for (long i = 0; i < N; i++) data[i] = 1;
/* Argument structs must persist until each thread is joined,
so they live in the main thread's stack. */
struct args args[WORKERS];
pthread_t tids[WORKERS];
size_t per = N / WORKERS;
double t0 = now();
for (int w = 0; w < WORKERS; w++) {
args[w].id = w;
args[w].start = w * per;
args[w].end = (w == WORKERS - 1) ? N : (w + 1) * per;
int err = pthread_create(&tids[w], NULL, sum_worker, &args[w]);
if (err != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(err));
exit(1);
}
}
for (int w = 0; w < WORKERS; w++) {
int err = pthread_join(tids[w], NULL);
if (err != 0) {
fprintf(stderr, "pthread_join: %s\n", strerror(err));
exit(1);
}
}
long total = 0;
for (int w = 0; w < WORKERS; w++) total += partial[w];
double t1 = now();
printf("total = %ld\n", total);
printf("elapsed = %.3f s\n", t1 - t0);
return 0;
}
Build and run:
gcc -Wall -Wextra -g -O2 -pthread -o psum psum.c
./psum
Output on a four-core VM:
thread 0: pid=1234, sum=100000000
thread 2: pid=1234, sum=100000000
thread 1: pid=1234, sum=100000000
thread 3: pid=1234, sum=100000000
total = 400000000
elapsed = 0.28 s
Three things are worth noting in the output.
- The order of the "thread" lines varies between runs. The scheduler does not guarantee that the threads run in the order they were created. Running the program several times will show different interleavings.
-
Every thread reports the same PID. On
Linux, all threads of a process share the same PID — the
thread group ID from Lesson 4.3 — even though each has its
own kernel task ID. To print the thread's own TID you
would need to call
gettid()(Linux-specific) or read it from/proc/self/task/. - The elapsed time is a fraction of the single-threaded time. On a four-core VM, four threads processing four times as much data in roughly the same time as one thread processing the whole array is what the multicore scaling from Lesson 4.4 looks like in practice.
Adding error handling and attributes
The example above uses the default attributes and simple error handling. A more complete version would validate every return value, use an attribute structure to configure a smaller stack for the workers, and clean up the attributes after creation:
pthread_attr_t attr;
int err = pthread_attr_init(&attr);
if (err != 0) { /* handle */ }
/* 1 MB stack instead of the default 8 MB */
err = pthread_attr_setstacksize(&attr, 1 << 20);
if (err != 0) { /* handle */ }
/* Detached: the workers have nothing to return */
err = pthread_attr_setdetachstate(&attr,
PTHREAD_CREATE_DETACHED);
if (err != 0) { /* handle */ }
for (int w = 0; w < WORKERS; w++) {
err = pthread_create(&tids[w], &attr, sum_worker, &args[w]);
if (err != 0) { /* handle */ }
}
pthread_attr_destroy(&attr);
/* Nothing to join — the workers are detached */
Note that the argument structs would need to outlive the workers in this version, because the parent no longer waits for them to complete. A common design is to make them global or heap-allocated. This is one of the consequences of moving to detached threads: the parent must not use stack-allocated memory that a detached thread might still be reading after the parent returns from its function.
Measuring the thread switch cost
As a final step, replace the sum loop with an empty loop
that calls sched_yield one million times in
each worker, and time the run. The result is a rough upper
bound on the kernel-thread switch cost, which we compared
against the user-thread switch from Lesson 4.2:
Kernel-thread switch (measured on a modern x86-64 VM):
Cost per switch: ~170 ns
User-thread switch (measured in Lesson 4.2):
Cost per switch: ~70 ns
The kernel-thread switch is roughly twice as expensive as the user-thread switch on this hardware. Both numbers are much lower than the raw process-switch measurement from Lesson 3.5 — the simplification is that the address space does not change, so the TLB and cache effects are much smaller.
06 Interactive activity
Activity · build, run, and stress the pthread API
Work through the following in your course VM. The goal is to use every function discussed in the lesson and to observe the behaviours — including the pitfalls — that matter most in practice.
-
Build the parallel sum.
Copy the
psum.cprogram from the worked example into your lab directory. Compile with the course flags and run it several times.
Does the ordering of the "thread" lines change between runs? What does this tell you about thread scheduling?gcc -Wall -Wextra -g -O2 -pthread -o psum psum.c ./psum ./psum ./psum -
Change the number of workers.
Modify the program to accept the number of workers on the
command line (use
atoionargv[1]). Run it with 1, 2, 4, and 8 workers. Record the elapsed times. Compute the speedup relative to the one-worker case at each thread count. -
Read the man page for
pthread_join.
Read the RETURN VALUE and ERRORS sections carefully. What does the man page say about joining a thread that has already been joined? What error code is returned?man 3 pthread_join -
Deliberately join a thread twice.
Modify the program to call
pthread_jointwice on the first thread. Compile and run. What does the second call return? Was the error reported clearly? -
Add a detached thread.
Write a small program that creates a thread, detaches it
using
pthread_detach, and then returns frommain. Note that if you return frommainwithout callingpthread_exit, the entire process terminates and the detached thread is killed immediately, even if it has not finished its work. Verify this by having the detached thread print something after a short sleep. -
Use an attribute structure.
Modify the parallel sum to use an attribute structure
that sets a 1 MB stack size and the detached state.
Note that the workers now cannot be joined. Ensure that the argument structs outlive the threads by making them global or heap-allocated. Rebuild and run.pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setstacksize(&attr, 1 << 20); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); /* ... create threads with &attr ... */ pthread_attr_destroy(&attr); - Introduce a race. Modify the program so that all workers increment a single shared counter (instead of writing to separate slots) a million times each. Compile without any locking and run several times. Compare the result to the expected value. Note that the discrepancy varies between runs. This is a preview of Unit 6, and it demonstrates why the "sharing" advantage of threads from Lesson 4.1 has a cost.
-
Inspect the threads of a running program.
Start the parallel sum with a large enough workload that
it takes several seconds. While it is running, open
another terminal and inspect it:
You will see the main thread plus the four workers, each with its ownPID=$(pgrep psum) ps -L -p $PID -o pid,tid,stat,pcpu,comm ls /proc/$PID/task/tidand state.
Reflect: which of the API functions was easiest to use? Which required the most care? Which of the pitfalls from section 4.10 did you encounter yourself, and how did you resolve it? If you were designing a new threading API today, what would you change about the pthreads interface?
07 Knowledge check
Check your understanding
Question 1
What is the exact signature required of a thread's start routine, and why does the API impose this signature?
Show answer
void *(*)(void *) — a function that takes
one void * argument and returns a
void *. The API imposes this because it
needs a uniform way to pass a single argument in and
receive a single value out, without knowing anything
about the types involved. The void *
argument lets the caller pass a pointer to anything —
typically a struct holding the thread's parameters —
and the void * return value lets the
thread report any result, which pthread_join
can then retrieve.
Question 2
What is the difference between a joinable thread and a detached thread, and when would you use each?
Show answer
pthread_join on its identifier. It is
appropriate when the parent needs the thread's result,
or needs to know when the thread has finished — for
example, a worker in a parallel computation whose
partial result is combined with others. A
detached thread's resources are
reclaimed automatically when it terminates. It is
appropriate when the work is fire-and-forget and no one
needs the result or the completion signal — for example,
a background logging task, or a request handler whose
result is delivered through another channel. Every
thread must be either joined or detached; a joinable
thread that is never joined leaks its resources.
Question 3
Why must a pthreads program be compiled with
-pthread, and what could go wrong if it is
not?
Show answer
-pthread flag does two things. First,
it defines the preprocessor macro _REENTRANT
(on some systems) or its equivalent, which enables the
thread-safe variants of some C library functions and
inline macros. Second, it links the program against
the threading library, which provides the implementations
of pthread_create and the other pthreads
functions. Without the flag, the program may fail to
link with unresolved symbols, or (worse) it may compile
and link but behave incorrectly under concurrency
because some library functions are not thread-safe in
the default configuration. Always compile pthreads
programs with -pthread, and consider
adding it to the Makefile so that no one can forget it.
Question 4
A program creates a thread in a loop, passing
&i as the argument where i
is the loop variable. The thread start routine casts the
argument to int and prints it. Almost every
thread prints the final value of i rather
than the value it had when the thread was created.
Explain what went wrong and how to fix it.
Show answer
i to every thread. When the thread runs,
it dereferences the pointer and reads whatever value
i currently holds — which, by the time any
thread actually executes, is likely the final value
after the loop terminated (or an arbitrary intermediate
value, depending on timing). The fix is to pass a
per-thread value, not the address of the loop variable.
Three common approaches: pass the value directly as a
void * (casting the integer to a pointer,
which works if it fits), allocate a per-thread argument
on the heap and pass its address, or use an array of
argument structs indexed by i. The
corresponding rule is that any memory a thread reads
through its argument must outlive the thread.
Question 5
Why does pthread_cancel not terminate the
target thread immediately, and what is the practical
consequence for programs that use it?
Show answer
pthread_cancel is a request, not a
command. The target thread will be terminated only when
it next reaches a cancellation point
— one of a defined set of functions, such as
read, write,
sleep, wait,
pthread_join, or
pthread_cond_wait. Between cancellation
points the thread continues running normally, and if it
never reaches one, it is never cancelled. This
design gives the target thread a chance to clean up
after itself: it can register cleanup handlers with
pthread_cleanup_push that run before the
thread is terminated, so locks are released, files are
closed, and memory is freed. The practical consequence
is that a program relying on cancellation must
identify the cancellation points in its threads, and
must ensure that no critical work is left in an
inconsistent state. Many programs avoid
pthread_cancel entirely and use a
cooperative flag instead, which is simpler to reason
about and does not depend on the placement of
cancellation points.
08 Unit discussion
Discussion prompt
The pthread API is a C interface: it uses
void * pointers, requires manual resource
management, and relies on the programmer to remember the
conventions for error handling, detach state, and joins.
Higher-level languages offer very different interfaces —
Java's Thread class, Python's
threading module, Rust's
std::thread, Go's goroutines — each with its
own balance of safety, expressiveness, and performance.
Imagine you are designing a new threading interface for systems programming in 2025. What design choices would you make differently from pthreads, and why? Consider: error handling, resource management, thread lifecycle, typing of thread arguments and results, and the interaction with modern language features such as ownership and async/await. Argue for the choices you would make.
Post your response to the Unit 4 discussion forum and reply to at least two classmates. This closes Unit 4. Supports CLO2, CLO6, and CLO8.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lab 3 and the thread portion of Assignment 1. Aim to spend 60–90 minutes on it.
- Write a pthreads program from scratch. Write a program that creates four threads, each of which sleeps for a different number of seconds (one, two, three, and four respectively) and then prints its own thread identifier along with the time it woke up. The main thread joins all four and prints a final message. Note that the output ordering is determined by the sleep times, not by the creation order.
-
Return values from threads.
Modify the program so each thread returns a value
indicating the number of seconds it slept. In the main
thread, retrieve each return value with
pthread_joinand print it. Note the technique used to pass an integer through avoid *. -
Compare joinable and detached. Rewrite
the program so that the four sleeping threads are
detached rather than joinable. Run it. What happens if
the main thread returns from
mainbefore the longest-sleeping thread has woken up? Then modify the main thread to sleep for five seconds before returning. Does the detached thread now finish? Explain what you observe. -
Configure attributes. Modify the program
to use an attribute structure that sets a 256 KB stack
size. Create a thousand threads with that attribute and
have them all sleep for a minute. While the program is
running, check the process's virtual memory usage:
Roughly how much virtual address space do a thousand 256 KB stacks consume? Compare to what the same program would consume with the default 8 MB stacks.grep VmSize /proc/<PID>/status -
Write a shell wrapper. Write a small
shell script that compiles and runs your pthread program,
printing the number of CPUs first and then the elapsed
time:
This script will be useful for the measurements in Lab 3.#!/bin/bash echo "cores: $(nproc)" gcc -Wall -Wextra -g -O2 -pthread -o prog prog.c for n in 1 2 4; do ./prog $n done -
Read a man page carefully. Read
man 7 pthreadsfrom beginning to end. List every function it mentions that you have not used, and for each one write a one-sentence description of what it does. This gives you a map of the API's full surface, so you know what exists when you need it later. - Reflect. In your own words, describe the single most important convention in the pthread programming model — the one that, if you forget it, will cause the most trouble — and explain why it matters.
10 Summary
- POSIX Threads (pthreads) is the standard C interface for threading on UNIX-like systems, including Linux, macOS, and the BSDs.
-
The core functions are
pthread_create(create a thread),pthread_join(wait for a joinable thread),pthread_detach(mark a thread as detached), andpthread_exit(terminate the calling thread). -
A thread's start routine has the fixed signature
void *(*)(void *). Arguments and return values pass throughvoid *pointers, usually to a struct that holds multiple values. -
Every thread is either joinable (its
resources are reclaimed by
pthread_join) or detached (its resources are reclaimed automatically on exit). A joinable thread that is never joined leaks its resources. -
The
pthread_attr_tstructure configures a thread's initial state: detach state, stack size, stack address, scheduling policy, and guard size. Set attributes before callingpthread_createto avoid races. -
Pthreads functions return error codes directly; they do not
set
errno. Usestrerror(err), notperror. -
Always compile pthread programs with
-pthread, which enables thread-safe library variants and links the threading implementation. -
Thread-local storage (via
_Thread_localor thepthread_key_createAPI) gives each thread a private copy of a variable that appears at file scope. It is what makes the C library safe under concurrency. -
Common pitfalls: forgetting
-pthread, usingerrnofor pthreads errors, returning pointers to thread-local variables, passing the address of a loop variable, never joining joinable threads, sharing data without synchronization, assuming scheduling order, and callingexitinstead ofpthread_exit.
Unit 4 complete
This closes the thread unit. You now have both the theory — the thread abstraction, user and kernel models, multicore scaling — and the practice: the pthread API that turns the theory into working code. Unit 5 turns to scheduling, and Unit 6 to the synchronization mechanisms that make multithreaded programs correct.
11 References
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapter 27.
Available online at
ostep.org. - Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 4, Section 4.4.
- Butenhof, D. R. Programming with POSIX Threads, Chapters 1–3. Addison-Wesley, 1997.
- Drepper, U., & Molnar, I. The Native POSIX Thread Library for Linux. Red Hat, 2003.
-
Linux man-pages:
man 7 pthreads,man 3 pthread_create,man 3 pthread_join,man 3 pthread_detach,man 3 pthread_exit,man 3 pthread_attr_init,man 3 pthread_key_create,man 3 pthread_self,man 3 pthread_equal. - POSIX.1-2017 / IEEE Std 1003.1, Standard for Information Technology — Portable Operating System Interface (POSIX), Section on Threads.