79 Running in separate strands — <threads.h>
What to know first
main · where a program starts and endsLooking back
Chapter 12 said that when the clock could go no higher the answer was to put in more cores. Yet every program in this book so far has had one strand of execution. On a machine with eight cores, what was that program doing?
A. Leaving seven of them idle. And that is not a mistake — adding strands has a price to pay the moment you add them. This chapter shows the price. Making a strand takes two functions; but the instant two of them touch the same memory, the “load, compute, store” you have learned so far comes apart. Watching it come apart is half of this chapter; the other half is putting it back together with locks and condition variables.
The need for this chapter, and its context
By the end of this chapter
mtx_*), condition variables (cnd_*), run-once (call_once) and per-strand storage complete the concurrency vocabulary the standard defines. Finally we ask why practice still uses pthread — the second instance, after Annex K in chapter 78, of “getting into the standard is not the same as winning”.The questions this chapter answers
- But I hear this header is one an implementation need not provide.
- Then why learn this chapter at all?
79.1 Why threads entered the standard
C89 had none, nor did C99. To split into strands you had to ask the operating system, and the words for that request were pthread on Unix and CreateThread on Windows. Running one program in both places meant writing it twice or wrapping it yourself.
It is exactly as chapter 12 described — when the clock hit a wall in the mid-2000s, performance came from core count, and “splitting into strands” turned from a specialist skill into an ordinary need. C11′s <threads.h> is the answer to that. The same edition brought <stdatomic.h> (chapter 80) as well — the vocabulary for making strands and the vocabulary for governing shared memory, handed over together.
Q. But I hear this header is one an implementation need not provide.
A. So it is; the standard says as much. An implementation that defines __STDC_NO_THREADS__ may omit <threads.h> and still conform. That is why this book’s examples look for that macro first.
★ This is the seed of the argument at the end of the chapter. A feature the standard itself calls optional is a hard thing for a library to lean on. You cannot ship code that only runs where it exists, so you end up compiling conditionally — or just using pthread.
79.2 The first strand — making one and waiting
examples-en/ch79/threads_basic.c
/* Running in separate strands — create, wait, and watch a value go wrong. */
#include <stdio.h>
#include <stdlib.h>
#ifdef __STDC_NO_THREADS__
/* The standard says an implementation may omit this header. This file still compiles there. */
int main(void) { puts("this implementation does not provide <threads.h>"); return 0; }
#else
#include <threads.h>
/* -- A thread function has exactly one shape: int (*)(void *) ------------- */
static int greet(void *arg)
{
const char *who = arg;
printf(" hello from %s\n", who);
return 7; /* thrd_join collects this */
}
/* -- Reproducing a race: several strands touch one cell, unprotected -------
The volatile is NOT here to fix anything. Without it the compiler folds the
whole loop into a single register and there is *no window for a race at all*
(that is what happened -- nothing was lost). volatile only makes each turn a
real load and store; it does not make the update safe. That is the next
chapter's story. */
#define BUMPS 200000
static volatile long shared;
static int bump(void *arg)
{
(void)arg;
for (int i = 0; i < BUMPS; i++) shared += 1; /* load, add, store */
return 0;
}
int main(void)
{
puts("[1] one thread, one return value");
thrd_t t;
if (thrd_create(&t, greet, "the worker") != thrd_success) {
fputs("thrd_create failed\n", stderr);
return EXIT_FAILURE;
}
int rc = 0;
thrd_join(t, &rc); /* wait for it to finish and take the value */
printf(" the worker returned %d\n\n", rc);
puts("[2] four threads bumping one counter, unprotected");
enum { N = 4 };
thrd_t w[N];
shared = 0;
for (int i = 0; i < N; i++) thrd_create(&w[i], bump, NULL);
for (int i = 0; i < N; i++) thrd_join(w[i], NULL);
long expected = (long)N * BUMPS, got = shared;
printf(" expected %ld\n", expected);
printf(" actual %ld%s\n", got, got == expected ? "" : " <- updates were lost");
puts(" (the number differs on every run --- that is what a race looks like)");
return 0;
}
#endif
Output
[1] one thread, one return value
hello from the worker
the worker returned 7
[2] four threads bumping one counter, unprotected
expected 800000
actual 295775 <- updates were lost
(the number differs on every run --- that is what a race looks like)
The first group of output is the whole vocabulary.
A thread function has exactly one shape — int (*)(void *). One argument in, one int out; if you need to pass several things, make a struct and hand over its address (chapter 46). The int it returns comes out through thrd_join’s second argument.
| Function | What it does | Worth knowing |
|---|---|---|
thrd_create(&t, fn, arg) | makes a strand and starts fn(arg) at once | returns thrd_success, thrd_nomem or thrd_error — which you must check |
thrd_join(t, &rc) | waits for it and takes the return value | pass null if you do not want rc |
thrd_detach(t) | declares that you will not wait | a detached strand cannot be joined |
thrd_current() | a handle on the current strand | compare only with thrd_equal — it is not an integer |
thrd_yield() | gives up the slot | a hint, not a guarantee |
thrd_sleep(&dur, &rem) | sleeps | two struct timespec (chapter 74) |
thrd_exit(res) | ends this strand only | called in main it does not end the program — it waits for the others |
Table 80.1
★ Do not treat a thrd_t as an integer. The standard does not say what it is, comparison goes through thrd_equal alone, and the standard offers no way to print one.
79.3 And then the value comes out wrong
The second group of output is the most important screen in this chapter. Four strands each added 1 two hundred thousand times, so it should be 800,000; the five runs this book made gave between 230,000 and 290,000. Two of every three increments vanished.
The cause is that one line is not one step.
What shared += 1 really does | When two strands overlap |
|---|---|
| both read 100 |
| both make 101 |
| both write 101 — two additions, one increment |
Table 80.2
A common misconception. “+= 1 happens in one go, so it is safe”
Being one line in the source and being one step on the machine are separate things (chapter 13). And even one step would not be safe — each core holds its own cache (chapter 11), so even “who wrote first” is not settled.
★ In the standard’s words this is a data race, and a data race is undefined behaviour. Not “the value comes out a bit wrong” but anything at all may happen (chapter 52).
In practice. What trying to reproduce the race taught us — the compiler erased it first
When this example was first written the value never came out wrong. Five runs, five times exactly 800000.
Not because there was no race but because there was no window for one. The compiler folded the two-hundred-thousand-turn loop into a single register and made it “add 200000 once” (chapter 13). Each strand touched memory effectively once, so there was nothing to overlap.
So the example’s shared was given a volatile. ★ Not to fix anything — to make every turn a real load and store so that the race could happen at all. volatile does not make the update safe. Why it does not is the first section of the next chapter.
79.4 Mutual exclusion — mtx_*
examples-en/ch79/threads_sync.c
/* Sleeping and waking — mutual exclusion, condition variables, once, per-strand. */
#include <stdio.h>
#include <stdlib.h>
#ifdef __STDC_NO_THREADS__
int main(void) { puts("this implementation does not provide <threads.h>"); return 0; }
#else
#include <threads.h>
/* -- 1: mutual exclusion -- the previous race, stopped by a lock ---------- */
#define BUMPS 200000
static long guarded; /* no volatile needed -- the lock gives the ordering */
static mtx_t lock;
static int bump_guarded(void *arg)
{
(void)arg;
for (int i = 0; i < BUMPS; i++) {
mtx_lock(&lock);
guarded += 1;
mtx_unlock(&lock);
}
return 0;
}
/* -- 2: a condition variable -- "wait until there is something" ----------- */
static mtx_t qlock;
static cnd_t not_empty;
static int items; /* the smallest possible queue: just a count */
static bool closed;
static int consumer(void *arg)
{
int *taken = arg;
mtx_lock(&qlock);
for (;;) {
/* while, not if -- waking up does not mean the condition holds */
while (items == 0 && !closed)
cnd_wait(¬_empty, &qlock);
if (items == 0 && closed) break;
items -= 1;
*taken += 1;
}
mtx_unlock(&qlock);
return 0;
}
/* -- 3: once -- lazy initialization --------------------------------------- */
static once_flag once = ONCE_FLAG_INIT;
static int init_count;
static void init_table(void) { init_count += 1; }
static int touch(void *arg)
{
(void)arg;
call_once(&once, init_table);
return 0;
}
/* -- 4: one copy per strand ------------------------------------------------ */
static thread_local int mine; /* a C23 keyword (chapter 82) */
static int per_thread(void *arg)
{
mine = *(int *)arg; /* each strand has its own */
return mine;
}
int main(void)
{
enum { N = 4 };
thrd_t w[N];
puts("[1] the same counter, now guarded by a mutex");
mtx_init(&lock, mtx_plain);
guarded = 0;
for (int i = 0; i < N; i++) thrd_create(&w[i], bump_guarded, NULL);
for (int i = 0; i < N; i++) thrd_join(w[i], NULL);
printf(" expected %ld, actual %ld%s\n", (long)N * BUMPS, guarded,
guarded == (long)N * BUMPS ? " <- nothing lost, every run" : " <- lost");
mtx_destroy(&lock);
puts("\n[2] a condition variable: wait until there is something");
mtx_init(&qlock, mtx_plain);
cnd_init(¬_empty);
int taken = 0;
thrd_t c;
thrd_create(&c, consumer, &taken);
for (int i = 0; i < 5; i++) {
mtx_lock(&qlock);
items += 1;
cnd_signal(¬_empty); /* signal while holding the lock */
mtx_unlock(&qlock);
}
mtx_lock(&qlock);
closed = true;
cnd_broadcast(¬_empty); /* tell everyone we are done */
mtx_unlock(&qlock);
thrd_join(c, NULL);
printf(" produced 5, consumer took %d\n", taken);
cnd_destroy(¬_empty);
mtx_destroy(&qlock);
puts("\n[3] call_once: four threads, one initialization");
for (int i = 0; i < N; i++) thrd_create(&w[i], touch, NULL);
for (int i = 0; i < N; i++) thrd_join(w[i], NULL);
printf(" init ran %d time(s)\n", init_count);
puts("\n[4] thread_local: each thread has its own");
int vals[N];
int got[N];
for (int i = 0; i < N; i++) { vals[i] = (i + 1) * 10; thrd_create(&w[i], per_thread, &vals[i]); }
for (int i = 0; i < N; i++) thrd_join(w[i], &got[i]);
printf(" each thread saw:");
for (int i = 0; i < N; i++) printf(" %d", got[i]);
printf("\n main's own copy is still %d\n", mine);
return 0;
}
#endif
Output
[1] the same counter, now guarded by a mutex
expected 800000, actual 800000 <- nothing lost, every run
[2] a condition variable: wait until there is something
produced 5, consumer took 5
[3] call_once: four threads, one initialization
init ran 1 time(s)
[4] thread_local: each thread has its own
each thread saw: 10 20 30 40
main's own copy is still 0
The first group is the answer. Wrap the same arithmetic in a lock and 800,000 comes out, and it does not vary however many times you run it.
mtx_lock(&lock);
guarded += 1;
mtx_unlock(&lock);What a lock gives is “one at a time”. And it gives one thing more as a side-effect — a lock also supplies ordering. That is why guarded needs no volatile.
| Kind | What differs | When |
|---|---|---|
mtx_plain | an ordinary lock | the default. Usually this |
mtx_timed | mtx_timedlock can bound the wait | when blocking forever would be a problem |
mtx_plain \| mtx_recursive | the same strand may lock it repeatedly | ★ usually a sign the design has knotted |
mtx_timed \| mtx_recursive | both |
Table 80.3
| Function | Worth knowing |
|---|---|
mtx_init(&m, type) | always before use. There is no static initializer in the standard (nothing like POSIX’s PTHREAD_MUTEX_INITIALIZER) |
mtx_lock(&m) | waits until it can lock |
mtx_trylock(&m) | if already locked, returns thrd_busy instead of waiting |
mtx_timedlock(&m, &ts) | only on one made mtx_timed. The time is absolute |
mtx_unlock(&m) | ★ unlocked by the strand that locked it |
mtx_destroy(&m) | when finished |
Table 80.4
Counter-example. Returning from inside a lock
mtx_lock(&m);
if (bad) return -1; /* ★ leaves it locked --- nobody can get in again */
mtx_unlock(&m);Chapter 41′s goto cleanup pattern earns its keep here. Where there are several ways out, gather the unlocking into one place. C has no automatic release as other languages do, so this is a discipline a person must keep — chapter 87′s cleanup pattern treats the same problem.
79.5 Waiting — cnd_*
A lock gives “one at a time”. But what is often needed is something else — “wait until there is something”. That is what a condition variable is for.
The second group of the example is the smallest possible queue. The consumer sleeps when there is nothing, and the producer wakes it as it puts something in.
mtx_lock(&qlock);
while (items == 0 && !closed) /* ★ while, not if */
cnd_wait(¬_empty, &qlock);★ That it is a while is the heart of this section. cnd_wait returning is no guarantee that the condition holds. Two reasons.
- Another strand may have taken it first. There is a gap between waking and reacquiring the lock.
- Spurious wakeups are permitted by the standard — you can wake with nobody having signalled.
So the rule is one line: check the condition again after waking.
| Function | Worth knowing |
|---|---|
cnd_wait(&c, &m) | releases the lock and sleeps; on waking it reacquires the lock before returning — you must hold the lock when calling |
cnd_timedwait(&c, &m, &ts) | thrd_timedout when the deadline passes. Absolute time |
cnd_signal(&c) | wakes one waiter |
cnd_broadcast(&c) | wakes all of them. For announcing that it is over |
cnd_init, cnd_destroy | as for a lock |
Table 80.5
79.6 Once only — call_once
Lazy initialization is a classic trap in concurrency. Write “make it if it is not made yet” naively and two strands both see “not made yet” and both make it.
static once_flag once = ONCE_FLAG_INIT;
call_once(&once, init_table); /* however many call it, init_table runs once */The third group of the example confirms it — four strands called, and initialization ran once. The function passed must take nothing and return nothing (void (*)(void)).
79.7 One per strand — thread_local and tss_*
Chapter 44 divided storage duration into four, and one of them was thread storage duration. This is where it is actually used.
| Way | Form | When |
|---|---|---|
thread_local | static thread_local int mine; | ★ usually this. A language feature, so cheap and easy to read (chapter 82) |
tss_* | make a key with tss_create(&key, dtor), then tss_get and tss_set | when a destructor is needed — something to clean up as the strand ends |
Table 80.6
In the fourth group of the example the four strands saw 10, 20, 30 and 40, while main’s own copy stayed 0. One name, as many objects as there are strands.
tss_* function | Worth knowing |
|---|---|
tss_create(&key, dtor) | dtor may be null. It runs at strand exit for non-null values |
tss_get(key) / tss_set(key, p) | the value is one void *, no more |
tss_delete(key) | discards the key — destructors do not run for values left behind |
TSS_DTOR_ITERATIONS | how many rounds if a destructor sets a value again |
Table 80.7
79.8 ★ So why does practice still use pthread?
It has been in the standard for over a decade, yet real C code still uses pthread or Win32. To end this chapter as more than an API tour, that question needs an answer.
| Reason | Evidence |
|---|---|
| 1. The standard said it is optional | __STDC_NO_THREADS__. The burden of checking and branching lands on the library author |
| 2. It arrived late | C11 is 2011; glibc added it only in 2.28, in 20181 — in those seven years everyone had already written it with pthread |
| 3. ★ It is missing where it matters | macOS provides no <threads.h> and yet does not define __STDC_NO_THREADS__ — the very detection the standard provided does not work there |
| 4. It is thin | cancellation, attributes, read-write locks, barriers, priorities — none of pthread’s are there. Nobody already using it has a reason to move |
Table 80.8
In practice. It works on this machine — and that is the trap
In this book’s verified environment (glibc 2.41) __STDC_NO_THREADS__ is not defined, and the examples compile and run with no special options at all. Here thrd_t was 8 bytes, mtx_t 40, cnd_t 48 and once_flag 4.
★ But working is not evidence of portability. Row 3 above is the example — build the same code on macOS and the header is not even there, and because the detection macro the standard defined is absent, no #ifdef filters it out. “It works on my machine” is one of the most dangerous places there is (chapter 12′s grey area).
Q. Then why learn this chapter at all?
A. For three reasons.
First, this is the minimum vocabulary the standard settled. pthread and Win32 do the same jobs — make, wait, lock, wait-and-wake. Learn those four under the standard’s names and the other APIs read as the same things spelled differently. The correspondence is very nearly one to one.
| C11 | POSIX | Win32 |
|---|---|---|
thrd_create | pthread_create | CreateThread |
thrd_join | pthread_join | WaitForSingleObject |
mtx_lock | pthread_mutex_lock | EnterCriticalSection |
cnd_wait | pthread_cond_wait | SleepConditionVariableCS |
call_once | pthread_once | InitOnceExecuteOnce |
thread_local | pthread_key_* | __declspec(thread) |
Table 80.9
Second, the next chapter leans on it. Atomic operations mean something only when there are strands.
Third, ★ this is the second telling of chapter 78′s Annex K story. Getting into the standard and being used by the world are separate things. What separates them — the time of arrival, the existing investment, and the clause that says “you need not”. Reading a standard with those three in view, not just the clauses, is the eye this book is trying to train.
79.9 Where to use it and where not to
| Situation | Advice |
|---|---|
| A small tool of your own, portability no concern | <threads.h> is enough — the standard alone will do |
| Targeting Linux, macOS and the BSDs together | ★ pthread. The macOS situation decides it |
| Windows as well | write a thin wrapper yourself, or take one that exists |
| First, are strands really needed? | ★ Usually no. Other ways of overlapping work (coroutines, event loops) are often cheaper (chapter 94) |
| If sharing can be reduced | reduce it. Not sharing is the cheapest synchronization there is |
Table 80.10
Recap
- A thread function has exactly one shape,
int (*)(void *), andthrd_joincollects its return value. - Touch one cell unprotected and increments really do vanish — this book measured it. In the standard’s words that is a data race, and a data race is undefined behaviour.
mtx_*gives both “one at a time” and ordering. Where there are several ways out, gather the unlocking into one place.- The check after
cnd_waitis awhile, not anif. - ★ Being in the standard does not mean you can use it — macOS gives neither the header nor the detection macro. Where portability matters, look to
pthread.
Notes
- glibc 2.28 NEWS: “Support for ISO C threads (ISO/IEC 9899:2011) has been added.”
sourceware.org, glibc NEWS ↩