Proven C Book한국어 GitHub

79 Running in separate strands — <threads.h>

What to know first

chapter 12, The machinery of speed · the clock’s limit and the turn to many cores
chapter 44, Lifetime and storage duration · what is shared and what belongs to each strand
chapter 53, The three faces of main · where a program starts and ends

Looking 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

Where the other chapters of part 11 read closely what we were already using, this one opens something never used at all. It is here because of the next chapter — the atomic operations of chapter 80 only mean something when there are several strands, and without knowing how to make one, that chapter’s demonstration is somebody else’s story. So how to make them comes first and how to share safely second. The floor is already laid: chapter 12 gave the many cores, chapter 44 gave “what is shared”, chapter 11 gave the per-core caches.

By the end of this chapter

We learn to create a strand (a thread), wait for it, and take back its value. Then we watch, by measurement, what actually happens when two of them touch one cell unprotected. Locks (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

  1. But I hear this header is one an implementation need not provide.
  2. 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 shapeint (*)(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.

FunctionWhat it doesWorth knowing
thrd_create(&t, fn, arg)makes a strand and starts fn(arg) at oncereturns thrd_success, thrd_nomem or thrd_errorwhich you must check
thrd_join(t, &rc)waits for it and takes the return valuepass null if you do not want rc
thrd_detach(t)declares that you will not waita detached strand cannot be joined
thrd_current()a handle on the current strandcompare only with thrd_equal — it is not an integer
thrd_yield()gives up the slota hint, not a guarantee
thrd_sleep(&dur, &rem)sleepstwo struct timespec (chapter 74)
thrd_exit(res)ends this strand onlycalled 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 doesWhen two strands overlap
  1. read the value from memory
both read 100
  1. add 1
both make 101
  1. write it back
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(&not_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(&not_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(&not_empty);       /* signal while holding the lock */
        mtx_unlock(&qlock);
    }
    mtx_lock(&qlock);
    closed = true;
    cnd_broadcast(&not_empty);        /* tell everyone we are done */
    mtx_unlock(&qlock);
    thrd_join(c, NULL);
    printf("  produced 5, consumer took %d\n", taken);
    cnd_destroy(&not_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.

KindWhat differsWhen
mtx_plainan ordinary lockthe default. Usually this
mtx_timedmtx_timedlock can bound the waitwhen blocking forever would be a problem
mtx_plain \| mtx_recursivethe same strand may lock it repeatedly★ usually a sign the design has knotted
mtx_timed \| mtx_recursiveboth

Table 80.3

FunctionWorth 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(&not_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.

  1. Another strand may have taken it first. There is a gap between waking and reacquiring the lock.
  2. 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.

FunctionWorth 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_destroyas 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.

WayFormWhen
thread_localstatic 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_setwhen 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_* functionWorth 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_ITERATIONShow 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.

ReasonEvidence
1. The standard said it is optional__STDC_NO_THREADS__. The burden of checking and branching lands on the library author
2. It arrived lateC11 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 mattersmacOS 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 thincancellation, 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.

C11POSIXWin32
thrd_createpthread_createCreateThread
thrd_joinpthread_joinWaitForSingleObject
mtx_lockpthread_mutex_lockEnterCriticalSection
cnd_waitpthread_cond_waitSleepConditionVariableCS
call_oncepthread_onceInitOnceExecuteOnce
thread_localpthread_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

SituationAdvice
A small tool of your own, portability no concern<threads.h> is enough — the standard alone will do
Targeting Linux, macOS and the BSDs togetherpthread. The macOS situation decides it
Windows as wellwrite 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 reducedreduce it. Not sharing is the cheapest synchronization there is

Table 80.10

Recap

  • A thread function has exactly one shape, int (*)(void *), and thrd_join collects 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_wait is a while, not an if.
  • ★ 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

  1. glibc 2.28 NEWS: “Support for ISO C threads (ISO/IEC 9899:2011) has been added.” sourceware.org, glibc NEWS