diff --git a/_docs/51-std-threading.md b/_docs/51-std-threading.md new file mode 100644 index 0000000..3e30311 --- /dev/null +++ b/_docs/51-std-threading.md @@ -0,0 +1,661 @@ +--- +title: "The standard threading library: what a lock costs, and why the answer is zero until it isn't" +order: 51 +part: "Compendium: C++ Concurrency" +description: "stdthread performs the identical 200,000 increments three ways -- with no mutex, with an uncontended std::mutex, and with the same mutex across eight threads -- and counts the syscalls each one issues with strace. The uncontended case matches the no-mutex baseline exactly at 1 futex call, which is the dynamic loader rather than a lock, while the contended case costs 1844-3111. std::condition_variable, std::latch, std::barrier, std::counting_semaphore, and std::atomic::wait are then each shown reaching the same futex(2), and a std::lock_guard deadlock is reproduced under timeout(1) with every task's /proc wchan reading futex_do_wait, against a std::scoped_lock version that completes 100,000 acquisitions in opposing orders. Verified on the Fedora 44 host with glibc 2.43: verify.lua PASS 38/FAIL 0, local, unprivileged, offline." +duration: "60 minutes" +--- + +Chapter 25 already settled the headline question. It built a bounded work queue +three ways, put `strace` on it, and showed a mutex's futex traffic scaling from +5,186 calls to 488,818 as contention rose — about 98% of lock operations +resolving as a pure userspace compare-exchange in the uncontended run. A mutex +is a futex; the fast path never leaves user space. + +This chapter starts from that result rather than re-deriving it, and pushes on +three things Chapter 25 did not cover. + +**First, how far "nearly free" actually goes.** Chapter 25's uncontended run +still made thousands of futex calls, because a work queue with condition +variables genuinely blocks. Strip the blocking away and measure a bare lock, +and the number is not "low" — it is *indistinguishable from not locking at +all*, which requires a different measurement technique to establish. + +**Second, the family.** C++20 added `latch`, `barrier`, `counting_semaphore`, +and `atomic::wait` to the `mutex` and `condition_variable` Chapter 25 used. +They look like a family of distinct facilities and they are one mechanism, and +seeing that is what stops you from reasoning about their costs separately. + +**Third, what the standard library does that the futex does not.** +`std::scoped_lock` will not deadlock where `std::lock_guard` will, and +`std::stop_token` answers the cancellation problem Chapter 50 ended on. Neither +is visible from the syscall layer. + +{% include excalidraw.html + file="51-futex-cost-model" + alt="Two bands showing the same 200,000 lock/unlock pairs measured two ways. The upper user-space band, labelled atomic compare-exchange on the futex word with no kernel involved, holds a baseline box for 200,000 plain increments with no mutex reporting 1 futex call and noting that the 1 is the loader rather than a lock; an amber uncontended box for one thread doing 200,000 lock/unlock pairs where the compare-exchange succeeds every time, also reporting 1 futex call, marked identical to the baseline; and a side note that the whole syscall profile is 61 calls of execve, mmap, openat, read and close, the dynamic loader, to which the 200,000 pairs contributed nothing. The lower amber kernel band, labelled futex(2) the syscall ch25 built by hand, holds an amber contended box for 8 threads doing the same 200,000 pairs where the compare-exchange fails often leading to sleep and wake, reporting 1844 to 3111 futex calls across 5 runs with the magnitude noted as varying; a box listing the blocking primitives condition_variable, latch, barrier, counting_semaphore and atomic::wait at 4 to 6 futex calls each; and a side note that these are five type names for one mechanism, because a thread that must sleep until further notice has to tell the kernel and there is no other way. An amber arrow labelled add contention runs from the uncontended box down to the contended box." + caption="Figure 51.1 — the same 200,000 lock/unlock pairs, two ways: uncontended locking is indistinguishable from no locking at all, and contention is what reaches the kernel" %} + +> **Tools used** — `g++` and `cmake` (host; gated by `scripts/check-host.sh` as +> `g++ >= 14` and `cmake >= 3.25`), `ninja` (host; gated), `clang++` (host; +> gated, parity gate G), **`strace`** (host; gated by `check-host.sh` — it is a +> hard gate here, not an illustration: gates B and C are syscall counts it +> produces), `timeout` from coreutils (host; used to bound the deliberate +> deadlock), `lua` (host; gated, runs `verify.lua`), and `python3` (host; +> gated, runs `scripts/test-all-examples.py`). No VM, no root, no LGTM stack, +> no network: `examples/51-std-threading` is `mode: local` in +> `examples/manifest.yaml`. + +C++-only, like the two chapters before it. Every code block below is a plain +fenced `cpp`, `lua`, or `console` block. + +## Making the comparison legitimate first + +A syscall count is only meaningful against another syscall count for the *same +work*. So the three cost cases are built to do exactly the same thing: + +```cpp +constexpr long kTotalOps = 200000; +constexpr int kContendedThreads = 8; +``` + +200,000 increments in every case. `baseline` does them with no mutex at all. +`uncontended` does them one at a time under a `lock_guard` on a single thread. +`contended` splits the same 200,000 across eight threads sharing one mutex. +Same operations, same final value, same digest — the only variable is how much +the threads collide. + +The harness asserts that before it compares anything: + +```lua +checks.expect_match(out["contended"], "acc=200000", + "cpp: contended performed the same 200000 increments across 8 threads -- same work, " + .. "so the futex counts below are comparable") +``` + +That assertion is not ceremony. A version of this example that accidentally +gave the contended case a different iteration count would still have produced a +dramatic-looking table, and the table would have meant nothing. + +## The baseline is not zero + +The obvious gate to write is "the uncontended case makes zero futex calls." It +would fail, and it *should* fail: + +``` +[host]$ strace -f -c -e trace=futex cpp/build/release/stdthread baseline +baseline futex=1 +``` + +That is the case with no mutex in it. A program that has linked libstdc++ has +already run the dynamic loader and glibc's startup before `main()` begins, and +somewhere in there is one futex call. Asserting zero would be asserting +something about the loader, not about locking. + +So the control exists precisely to be subtracted: + +```cpp +void case_baseline() { + long n = 0; + for (long i = 0; i < kTotalOps; ++i) { + ++n; + } + report("baseline", n); +} +``` + +## Uncontended locking is not "cheap" — it is absent + +``` +[host]$ for c in baseline uncontended contended; do + printf "%-12s futex=%s\n" "$c" \ + "$(strace -f -c -e trace=futex cpp/build/release/stdthread $c 2>&1 | awk '/futex/{print $4}')" + done +baseline futex=1 +uncontended futex=1 +contended futex=2360 +``` + +200,000 `lock_guard` acquisitions and releases, and the futex count is +identical to the run that never locked anything. + +That is not rounding or noise. Here is the *entire* syscall profile of the +uncontended run: + +``` +[host]$ strace -f -c cpp/build/release/stdthread uncontended 2>&1 | tail -3 + 0.00 0.000000 0 1 execve +------ ----------- ----------- --------- --------- ---------------- +100.00 0.000084 1 61 1 total +``` + +Sixty-one syscalls total, and every one of them is `execve`, `mmap`, `openat`, +`read`, or `close` — the dynamic loader starting the process. The 200,000 +lock/unlock pairs contributed nothing to it. + +The mechanism is Chapter 25's, unchanged: `std::mutex` is a `pthread_mutex_t` +is a futex word, and locking a free one is an atomic compare-exchange that +either succeeds in user space or fails into `futex(FUTEX_WAIT)`. What is new +here is the *strength* of the statement the measurement supports. Chapter 25 +could say 98% of acquisitions stayed in user space. This says 100% of them did, +and the way to establish that is the baseline subtraction above — comparing +against a control rather than against zero, so that the loader's syscalls do +not get charged to the mutex. + +```cpp +void case_uncontended() { + std::mutex m; + long n = 0; + for (long i = 0; i < kTotalOps; ++i) { + std::lock_guard g(m); + ++n; + } + report("uncontended", n); +} +``` + +This is worth holding onto, because it inverts a common instinct. Adding a +mutex to a data structure that is rarely touched by more than one thread at a +time does not cost you syscalls. It costs you an atomic operation and a cache +line. The advice "locks are expensive, avoid them" is measuring the wrong +thing; the accurate version is "*waiting* is expensive, avoid contention." + +## Contention is what reaches the kernel + +Same 200,000 pairs, eight threads: + +```cpp +void case_contended() { + std::mutex m; + long n = 0; + std::vector threads; + threads.reserve(kContendedThreads); + for (int t = 0; t < kContendedThreads; ++t) { + threads.emplace_back([&m, &n] { + for (long i = 0; i < kTotalOps / kContendedThreads; ++i) { + std::lock_guard g(m); + ++n; + } + }); + } + threads.clear(); // jthread joins on destruction + report("contended", n); +} +``` + +Now the compare-exchange fails constantly, and every failure is a trip into the +kernel to sleep on the futex word — plus another, on the unlock side, to wake a +waiter. + +Measured across five runs on the reference host: **1844, 2199, 2360, 2469, +3111**. The magnitude moves with scheduling and is not something to assert on. +The gate takes the sign: + +```lua + checks.expect_match(tostring(n_con ~= nil and n_unc ~= nil and n_con > n_unc * 100), "true", + "cpp: contended exceeds uncontended by more than two orders of magnitude") +``` + +Chapter 39's discipline applies to syscall counts exactly as it does to +durations: the number that varies run to run is not the finding. "More than two +orders of magnitude" has never once been false here; "2360" would be false on +the next run. + +One detail worth noticing: 200,000 lock operations produced roughly 2,400 futex +calls, not 200,000. Break those down by operation and the picture sharpens +further: + +``` +[host]$ strace -f -e trace=futex cpp/build/release/stdthread contended 2>&1 \ + | grep -oE "FUTEX_[A-Z_]+" | sort | uniq -c | sort -rn | head -3 + 877 FUTEX_WAKE_PRIVATE + 554 FUTEX_WAIT_PRIVATE + 4 FUTEX_WAIT_BITSET +``` + +**554 sleeps out of 200,000 acquisitions.** Under deliberate eight-way +contention, 99.7% of lock operations never reached the kernel at all — glibc +spins briefly before it sleeps, precisely because the syscall is the expensive +part. The `_PRIVATE` suffix is the same optimization Chapter 25 covered: a +futex known not to be shared across processes skips the kernel's shared-mapping +lookup. + +## Five type names, one mechanism + +The C++20 additions look like a family of distinct facilities. Underneath they +are the same syscall: + +| case | primitive | futex calls (3 runs) | +| --- | --- | --- | +| `condvar` | `std::condition_variable::wait` | 5, 5, 5 | +| `latch` | `std::latch::arrive_and_wait` | 5, 8, 5 | +| `barrier` | `std::barrier::arrive_and_wait` | 5, 4, 6 | +| `semaphore` | `std::counting_semaphore::acquire` | 4, 4, 4 | +| `atomic-wait` | `std::atomic::wait` | 4, 4, 4 | + +This is not an implementation coincidence to be filed away. A thread that must +sleep until some other thread does something *cannot* be implemented in user +space alone — the whole point is to stop consuming CPU, and only the kernel can +deschedule you. Any primitive with that shape reaches a syscall, and on Linux +that syscall is `futex`. + +`std::atomic::wait` is the one that makes the relationship visible in the API +itself: + +```cpp + std::atomic flag{0}; + long n = 0; + std::jthread waiter([&] { + flag.wait(0); // block while flag == 0 + n = flag.load(); + }); + settle(); + flag.store(7); + flag.notify_one(); + waiter.join(); +``` + +`wait(0)` means "sleep while the value is still 0." That is `FUTEX_WAIT`'s +contract from Chapter 25 with the syscall boilerplate removed — the expected +value is passed in precisely so the kernel can re-check it atomically against +the sleep, closing the lost-wakeup race. C++20 standardized the futex, minus +the parts that are not portable. + +The `latch` and `barrier` distinction is worth one line since they are easy to +confuse: a latch counts down once and stays at zero, while a barrier resets +after each phase and can run a completion function on whichever thread trips +it: + +```cpp + std::atomic phases{0}; + std::barrier sync{2, [&phases]() noexcept { phases.fetch_add(1, std::memory_order_relaxed); }}; +``` + +The completion function must be `noexcept` — the standard requires it, because +there is no sensible place to deliver an exception thrown while every +participating thread is mid-rendezvous. + +### A note on the measurement scaffolding + +Each blocking case has to get its waiter into the wait before the signal +arrives, or it might measure nothing: + +```cpp +void settle() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} +``` + +That sleep is scaffolding, not measurement. No assertion anywhere depends on +its length — if it were too short the waiter would simply not block, the futex +count would drop to zero, and gate C would fail loudly rather than passing on a +technicality. It is deliberately named `settle` rather than something that +sounds like a timing parameter. + +## Deadlock, and the standard library's answer + +{% include excalidraw.html + file="51-deadlock-and-scoped-lock" + alt="Two mutexes and two threads producing two outcomes. The amber upper band, titled std::lock_guard in opposing orders as the textbook deadlock, shows thread 1 taking lock_guard x(a) then lock_guard y(b) and thread 2 taking lock_guard x(b) then lock_guard y(a) in the opposite order, leading to an amber box stating each holds what the other needs, timeout(1) exits 124 and it never returns, with no error, no exception, and no diagnostic. A dashed box below adds that the kernel names it via /proc//task/*/wchan, with tids 2744896, 2744897 and 2744898 all reading futex_do_wait. The lower band, titled std::scoped_lock as taking them all at once so order stops mattering, shows thread 1 with scoped_lock lock(a, b) and thread 2 with scoped_lock lock(b, a) still in the opposite order, leading to an amber box stating it runs try-all, back off, retry rather than argument order, that 100,000 acquisitions completed with exit 0, and that this is the property lock_guard cannot give you." + caption="Figure 51.2 — the textbook two-mutex deadlock reproduced and observed in procfs, beside the std::scoped_lock version that survives the same opposing orders" %} + +Two mutexes, two threads, opposing acquisition orders. The textbook says this +deadlocks. The textbook is right: + +```cpp + std::thread t1([] { + for (;;) { + std::lock_guard x(a); + std::this_thread::sleep_for(std::chrono::microseconds(1)); + std::lock_guard y(b); + } + }); +``` + +``` +[host]$ timeout 5 cpp/build/release/stdthread deadlock-naive; echo "exit=$?" +deadlock-naive: two lock_guards in opposing orders; this will hang +exit=124 +``` + +Exit 124 is `timeout(1)` reporting that it had to kill the process. Note what +did *not* happen: no error, no exception, no diagnostic, no return code from +any lock operation. A deadlocked program is not a failing program — it is a +program that stopped, silently, forever. That is what makes deadlock hard to +diagnose from inside the process. + +From outside, the kernel is perfectly willing to say what happened: + +``` +[host]$ p=$(pgrep -x stdthread) +[host]$ for t in /proc/$p/task/*; do + printf "tid=%-8s state=%-2s wchan=%s\n" "$(basename $t)" \ + "$(awk '/^State:/{print $2}' $t/status)" "$(cat $t/wchan)" + done +tid=2744896 state=S wchan=futex_do_wait +tid=2744897 state=S wchan=futex_do_wait +tid=2744898 state=S wchan=futex_do_wait +``` + +`wchan` is the kernel function a sleeping task is blocked in, and Chapter 36 +read this file for other purposes. Here it closes the loop that Figure 51.1 +opened: the abstraction was `std::mutex`, the failure is `futex_do_wait`, and +you can read it with `cat`, unprivileged, with no debugger attached. + +Three threads park, not two — the two workers plus the main thread blocked in +`join()`. Every one of them is waiting on a futex that no one will ever wake. + +### `scoped_lock` is not `lock_guard` with more arguments + +```cpp + std::jthread t2([&] { + for (long i = 0; i < kDeadlockIters; ++i) { + // The OPPOSITE order, deliberately. scoped_lock does not care. + std::scoped_lock lock(b, a); + done.fetch_add(1, std::memory_order_relaxed); + } + }); +``` + +``` +[host]$ ./demo.sh cpp run deadlock-safe +deadlock-safe: completed 100000 acquisitions in opposing orders +``` + +`std::scoped_lock` with two or more mutexes does not lock them in argument +order. It runs a deadlock-avoidance algorithm — lock the first, *try* the rest, +and on any failure release everything and start again from a different one. So +`scoped_lock(a, b)` and `scoped_lock(b, a)` running concurrently cannot +deadlock, and the argument order genuinely does not matter. + +That is the property `lock_guard` cannot give you no matter how carefully you +write it, and it is the reason `scoped_lock` exists as a separate type rather +than as a variadic `lock_guard`. The single-mutex case is the one where they +are interchangeable. + +The convention this leaves you with: use `lock_guard` (or `scoped_lock`) for +one mutex, `scoped_lock` for two or more, and never take a second mutex inside +a scope that already holds one unless you can state the global lock order out +loud. + +## The `stop_token` payoff + +Chapter 50 ended on an argument: `pthread_cancel` unwinds through an exception +that an ordinary `catch (...)` can fatally swallow, and C++20 answered with +cooperative cancellation instead. Here is what that answer looks like running: + +```cpp +void case_stoptoken() { + std::atomic loops{0}; + std::jthread worker([&loops](std::stop_token stop) { + while (!stop.stop_requested()) { + loops.fetch_add(1, std::memory_order_relaxed); + std::this_thread::yield(); + } + }); + + settle(); + worker.request_stop(); + worker.join(); +``` + +``` +[host]$ ./demo.sh cpp run stoptoken +stoptoken: stop_requested honored, worker returned normally +stdthread report: case=stoptoken acc=1 digest=0x481984990deee5ff +``` + +The worker returned. Not cancelled, not unwound, not `PTHREAD_CANCELED` — +it reached the top of its loop, saw the flag, and left through the normal exit. +Nothing was thrown, so there is nothing a catch-all can swallow, and there is +no list of cancellation points to memorize. + +The trade is real and worth stating plainly: this does strictly less than +`pthread_cancel`. A thread blocked in a `read(2)` that never checks the token +will not stop, and no amount of `request_stop()` will move it. Chapter 12's +self-pipe and Chapter 13's `pidfd` are the shapes you reach for when a blocked +thread has to be interruptible; `stop_token` is for threads that loop. + +Note also that `jthread` joins in its destructor and requests a stop first, +which is why `case_contended` can do this: + +```cpp + threads.clear(); // jthread joins on destruction +``` + +That single line is a `join()` on eight threads. `std::thread`'s destructor +calls `std::terminate` if the thread is still joinable — a design that catches +the bug loudly, at the cost of making every early return a leak hazard. +`jthread` is the fix, and the reason to reach for it by default. + +## How the code works + +Each measurement is its own subcommand: + +```cpp + if (what == "baseline") { + case_baseline(); + } else if (what == "uncontended") { + case_uncontended(); + } else if (what == "contended") { + case_contended(); +``` + +The structure carries weight for the same reason it did in Chapter 50: +`deadlock-naive` never returns. If the cases shared a process, that one would +hang the whole program and the harness would time out on everything. + +The deadlock case is marked so the compiler knows: + +```cpp +[[noreturn]] void case_deadlock_naive() { +``` + +The `std::abort()` at the end of that function is unreachable — the joins never +return — and is there so the `[[noreturn]]` contract holds on every path a +compiler can see. + +The digest is carried forward unchanged from Chapters 46 through 50: + +```cpp +void report(const char* what, long acc) { + std::printf("stdthread report: case=%s acc=%ld digest=0x%016llx\n", what, acc, + static_cast(fnv1a(kPayload, sizeof kPayload))); +} +``` + +`acc` is printed beside it deliberately. The digest proves the payload was +hashed; `acc` proves the case actually did its work, which is what makes the +syscall comparison legitimate. + +## Errors, three ways + +**A failure with no error to report.** Deadlock is the pure case: no return +code, no exception, no `errno`. The only signal is that time passes and nothing +happens. This is why the gate is a `timeout` exit status and a procfs +reading — there is nothing inside the process to check. + +**A failure the harness must not turn into a false pass.** `strace` can be +blocked by ptrace hardening, and `/proc//wchan` can be unreadable. Both +would make the interesting gates unmeasurable, and both degrade to a printed +`SKIP`: + +```lua +if not have_strace or n_base == nil then + print("SKIP: strace could not count syscalls (absent or ptrace-restricted) -- " .. + "gates B and C not asserted") +else +``` + +A skipped gate that announces itself is a different thing from a gate that +quietly passed, and Chapter 48 established the pattern: `SKIP:` lines beside the +`PASS` count. + +**A failure the standard library turns into a crash on purpose.** +`std::thread`'s destructor calls `std::terminate` on a still-joinable thread. +That is a deliberate choice to make a silent resource bug into a loud one, and +`jthread` exists so you rarely meet it. + +## Concurrency lens + +The counters in this example are `std::atomic` under `memory_order_relaxed`, +and the reasoning is the same as Chapter 49's: they are observation state, read +once after every thread has joined, with `join()` supplying the +synchronization edge. + +The exception is `case_contended`'s `long n`, which is a **plain** `long` — not +an atomic: + +```cpp + for (long i = 0; i < kTotalOps / kContendedThreads; ++i) { + std::lock_guard g(m); + ++n; + } +``` + +That is correct, and it is the whole point of the case. Every access happens +under the mutex, so the mutex provides the mutual exclusion *and* the +happens-before edges. Making it atomic would be redundant, and worse, it would +muddy the measurement: an atomic increment under a lock would still be an +atomic operation the uncontended case does not perform, and the two cases have +to differ in exactly one respect. + +That `acc=200000` in the output is what proves it. A racy increment would come +back short — that is the classic symptom — so the assertion doubles as a +correctness check on the locking itself. + +The deadlock cases use `std::thread` rather than `std::jthread` for the naive +version, deliberately. `jthread` would request a stop and join in its +destructor, which is the right default everywhere else and would obscure what +is being demonstrated here. + +## Build, run, observe + +``` +[host]$ cd examples/51-std-threading +[host]$ ./demo.sh cpp build +[host]$ ./demo.sh cpp run uncontended +[host]$ strace -f -c -e trace=futex cpp/build/release/stdthread contended +``` + +Do the counting yourself rather than trusting the table — that is the entire +skill this chapter is teaching, and `strace -c` is the whole tool. + +To watch the deadlock, run it under `timeout` in one terminal and read procfs +in another: + +``` +[host]$ timeout 5 cpp/build/release/stdthread deadlock-naive +[host]$ cat /proc/$(pgrep -x stdthread)/task/*/wchan +``` + +Then the harness: + +``` +[host]$ LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua +``` + +``` +ok: cpp: 200000 uncontended lock/unlock pairs cost no more futex calls than the no-mutex baseline (baseline=1, uncontended=1) -- an uncontended std::mutex is an atomic CAS, not a syscall +ok: cpp: the identical 200000 pairs, contended, cost hundreds of futex calls (contended=2426) -- the cost is contention, not locking +ok: cpp: contended exceeds uncontended by more than two orders of magnitude +ok: cpp: condvar blocks, and blocking means futex(2) (calls=5) +ok: cpp: std::scoped_lock(a,b) and scoped_lock(b,a) on two threads completed -- argument order does not matter to scoped_lock +ok: cpp: two lock_guards in opposing orders deadlock -- timeout(1) had to kill it +ok: cpp: while deadlocked, procfs reports every thread parked in a futex wait (wchan: futex_do_wait futex_do_wait futex_do_wait ) +ok: cpp: the jthread observed request_stop() and returned on its own terms -- no PTHREAD_CANCELED and no forced unwind, unlike ch50's pthread_cancel +info: absolute futex counts for the contended case are NOT gated -- they are scheduling-dependent (1844-3111 observed across runs on the reference host); only the sign of the comparison is asserted +PASS 38 / FAIL 0 +``` + +## Cross-check: one digest, one futex + +Every non-hanging case reported `digest=0x481984990deee5ff`, and clang agrees. +That constant now spans Chapter 46's C++ toolbox, Chapter 47's Go, Chapter 48's +Rust, Chapter 49's four execution models, Chapter 50's six per-thread controls, +and Chapter 51's eleven synchronization cases. + +There is a second cross-check running through this chapter, and it points +backwards. Chapter 25 measured a mutex's futex traffic scaling from 5,186 calls +to 488,818, and concluded that a mutex is a futex whose fast path stays in user +space. Chapter 26 built lock-free structures on atomics. This chapter agrees +with both and sharpens the first: with the blocking stripped away and a control +to subtract, the uncontended figure is not 98% but 100% — the mutex contributes +no syscalls whatsoever. + +That agreement across two chapters, two examples, and two measurement +techniques is worth more than either number alone. Chapter 25's workqueue and +this chapter's bare loop have almost nothing in common except the primitive +underneath, and they report the same structure. + +## What you learned + +- **An uncontended `std::mutex` costs no syscalls at all.** Chapter 25 + established that the fast path stays in user space; with the blocking + stripped away and a control to subtract, 200,000 lock/unlock pairs produced + the same futex count as a run with no mutex, and a 61-syscall profile that is + entirely loader startup. +- **Measure against a baseline, not against zero.** A program that has linked + libstdc++ makes syscalls before `main()` runs; the control case exists to be + subtracted. Asserting zero would have been asserting something about the + dynamic loader. +- **Contention is the cost, and even then most of it stays in user space.** The + identical 200,000 operations across eight threads cost 1844–3111 futex calls, + of which only 554 were actual `FUTEX_WAIT` sleeps — 99.7% of acquisitions + never reached the kernel, because glibc spins before it sleeps. +- **Gate the sign, not the magnitude.** "More than two orders of magnitude" is + reproducible; "2360" is not. Chapter 39's rule applies to syscall counts as + much as to durations. +- **Five type names, one mechanism.** `condition_variable`, `latch`, `barrier`, + `counting_semaphore`, and `atomic::wait` all reach `futex(2)`, because a + thread that must sleep until notified can only be descheduled by the kernel. +- **`std::atomic::wait(old)` is `FUTEX_WAIT`** with the expected value passed + in for the same reason — so the kernel can re-check it atomically against the + sleep and close the lost-wakeup race. +- **A deadlocked program is not a failing program.** No error, no exception, no + return code — it simply stops. From outside, `/proc//wchan` names the + kernel function every thread is parked in. +- **`std::scoped_lock` is not variadic `lock_guard`.** It runs a + deadlock-avoidance algorithm rather than locking in argument order, which is + why opposing orders across threads are safe with it and fatal without it. +- **`stop_token` does less than `pthread_cancel`, on purpose.** The worker + returns normally on its own terms; a thread blocked in a syscall that never + polls the token will not stop, and that limitation is the design. +- **Prefer `jthread`.** It requests a stop and joins in its destructor, where + `std::thread`'s destructor calls `std::terminate` on a joinable thread. + +--- + +

verified — on the Fedora 44 +reference host this session (kernel 7.1.5-201.fc44, glibc 2.43, +g++ 16.1.1 20260515, clang 22.1.8, CMake 4.3.0, Ninja 1.13.0, strace 7.1, Lua +5.4.8, 16 logical / 8 physical CPUs; local, unprivileged, no network, no VM): +LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua +reported PASS 38 / FAIL 0 with gates B, C, and E all running for +real rather than skipping, and python3 scripts/test-all-examples.py --only +51-std-threading reported 1 passed, 0 failed, 0 skipped. +Every transcript quoted above is a real run from this session. Confirmed live: +all ten non-hanging cases produced digest=0x481984990deee5ff, +byte-identical to Chapters 46 through 50; the three cost cases each reported +acc=200000, so the syscall counts compare equal work; futex counts +from strace -f -c -e trace=futex over three runs each were +baseline 1/1/1, uncontended 1/1/1, +contended 2360/2199/2469 (and 1844 and 3111 in earlier runs, the +range quoted in the text), condvar 5/5/5, latch 5/8/5, +barrier 5/4/6, semaphore 4/4/4, +atomic-wait 4/4/4, stoptoken 2/2/2; the full +strace -c profile of the uncontended run was 61 +syscalls total; deadlock-safe completed +100000 acquisitions in opposing orders; +timeout 5 … deadlock-naive exited 124 and, caught +mid-hang, all three tasks in /proc/<pid>/task/*/wchan read +futex_do_wait; and stoptoken printed +stop_requested honored, worker returned normally. Not exercised: +unverified — the absolute futex +count for the contended case is deliberately not gated anywhere, because it is +scheduling-dependent; only the sign of the comparison is asserted, and +verify.lua prints an informational line saying so. Two gates would +degrade to a printed SKIP on a host where strace +cannot attach (ptrace hardening) or /proc/<tid>/wchan is +unreadable; neither condition occurred here, so both paths are reasoned rather +than observed.

diff --git a/_plans/r20-ch51-plan.md b/_plans/r20-ch51-plan.md new file mode 100644 index 0000000..f29ec58 --- /dev/null +++ b/_plans/r20-ch51-plan.md @@ -0,0 +1,182 @@ +--- +title: "r20 / ch51 — std threading — plan (internal)" +published: false +--- + +# r20 ch51 — example `51-std-threading` + chapter + diagrams + +Third chapter of Part 14. ch49 set the vocabulary, ch50 went under `std::thread` to the +pthread. ch51 covers the standard library's synchronization primitives — and the risk +here is writing an API tour, which this book must not do. + +**The angle that makes it a systems chapter: what does each standard primitive become on +this kernel, and what does it cost?** The answer is a futex when it blocks and *nothing +at all* when it does not, and both halves are measurable with a syscall counter. + +## Decisions (answered 2026-08-17) + +- **D1 — futex-cost spine.** Organize around what each primitive becomes: the + uncontended-vs-contended mutex as the headline, then `condition_variable`, `latch`, + `barrier`, `counting_semaphore`, and `atomic::wait` each measured reaching the same + syscall. Extends ch25 (futexes) and ch26 (atomics) rather than restating them. +- **D2 — deadlock both ways, with a hard timeout.** Gate that `std::scoped_lock` + completes with opposing lock orders, AND that the naive version deadlocks under + `timeout` (exit 124) with every thread showing `wchan=futex_do_wait` in procfs. +- **D3 — Autonomous through PR; the user merges.** + +## Host audit (run 2026-08-17, Fedora 44, kernel 7.1.5-201.fc44, glibc 2.43) + +Measured with `strace -f -c -e trace=futex`, not inferred: + +| run | same total work | futex syscalls | +| --- | --- | --- | +| baseline: no mutex, no threads, 200k increments | 200,000 | **1** | +| `std::mutex` uncontended, 1 thread, 200k lock/unlock | 200,000 | **1** | +| `std::mutex` contended, 8 threads x 25k lock/unlock | 200,000 | **1844–3111** (5 runs) | + +**The single futex call in the uncontended run is not the mutex.** The baseline with no +mutex at all reports the same 1, and the uncontended run's *entire* syscall profile is +61 calls — `execve`, `mmap`, `openat`, `read`, `close`, i.e. the dynamic loader. 200,000 +lock/unlock pairs contributed nothing to it. Uncontended `std::mutex` is an atomic CAS +and no syscall whatsoever. + +Stability: uncontended reported exactly **1 on all 5 runs**. Contended varied 1844–3111 — +magnitude unstable, sign rock solid. Gate on the sign (see risks). + +Other primitives, futex calls per run: `condition_variable` 5, `latch` 6, +`counting_semaphore` 4, `jthread`+`stop_token` 2. + +### Deadlock, both directions (measured) + +| variant | result | +| --- | --- | +| `std::scoped_lock(a,b)` vs `scoped_lock(b,a)`, 50k iters each | **completes**, exit 0 | +| naive `lock_guard` in opposing orders | **deadlocks**, `timeout` exit **124** | + +While hung, every task in `/proc//task/*/`: + +``` +tid=2716241 state=S wchan=futex_do_wait +tid=2716242 state=S wchan=futex_do_wait +tid=2716243 state=S wchan=futex_do_wait +``` + +Three threads — the two workers and main blocked in `join` — all parked in the same +kernel function, readable unprivileged. `wchan` closes the loop: the abstraction's +failure mode, named in kernel terms. + +### Feature macros (GCC 16.1.1) + +`__cpp_lib_jthread` 201911, `__cpp_lib_latch` 201907, `__cpp_lib_barrier` 201907, +`__cpp_lib_semaphore` 201907, `__cpp_lib_atomic_wait` 201907, `__cpp_lib_shared_mutex` +201505, `__cpp_lib_scoped_lock` 201703, `__cpp_lib_hardware_interference_size` 201703 +(both destructive and constructive = **64**). + +`strace` is a **hard-gated tool** in `scripts/check-host.sh` (line ~229), so it may carry +a gate and be named in the tools box. + +## The core demo — `stdthread` (C++-only, `langs: [cpp]`) + +One subcommand per measurement, so the deadlock subcommand's deliberate hang can never +take the others with it. Digest continuity: `digest=0x481984990deee5ff`. + +Subcommands: `baseline`, `uncontended`, `contended`, `condvar`, `latch`, `barrier`, +`semaphore`, `atomic-wait`, `stoptoken`, `deadlock-safe`, `deadlock-naive`. + +## Gate tiers + +**Hard (A–F), all local, unprivileged, offline:** +- A. digest identical across every non-hanging subcommand. +- B. **the headline**: `uncontended` futex count <= 2 AND equal to `baseline`'s, while + `contended` (same 200,000 lock/unlock pairs) is > 500. Counted with + `strace -f -c -e trace=futex`. +- C. the blocking primitives each reach futex: `condvar`, `latch`, `barrier`, + `semaphore`, `atomic-wait` all report >= 1 futex call. +- D. `deadlock-safe` completes under `timeout` with exit 0. +- E. `deadlock-naive` times out (exit 124) AND every task in `/proc//task/*/wchan` + reads `futex_do_wait` while it is hung. +- F. `stoptoken` completes and the stop itself needs no unwinding (contrast ch50). + +**Gated-if-present (G):** clang parity on the digest. + +**Not gated:** absolute futex counts for the contended case (scheduling-dependent); +timings of any kind. + +## Risks + +- **Contended futex count is scheduling-dependent** (1844–3111 measured). Gate `> 500`, + never a magnitude. State the observed range in the chapter, per ch39. +- **`strace` may be denied by ptrace hardening** (`kernel.yama.ptrace_scope`). It is 0 or + 1 on this host and strace on a child we spawn works, but the gate should degrade to an + informational SKIP if strace cannot attach, never a FAIL. +- **The deadlock subcommand hangs by design.** It must (a) be its own subcommand, (b) be + invoked only under `timeout`, and (c) have the manifest timeout comfortably above it. + `verify.lua` must never invoke it bare. +- **`wchan` can read `0` or be denied** on some kernels/hardening. Measured + `futex_do_wait` here; gate loosely on `futex` appearing, and SKIP if wchan is + unreadable rather than FAIL. +- **The one futex call in the uncontended run must be attributed correctly.** The gate + compares it to the baseline rather than asserting zero, because asserting zero would + be wrong — and the chapter must explain that it is loader startup, not the mutex. + +## Steps + +- S1 mint `examples/51-std-threading`, strip go/rust, single-lang +- S2 write `cpp/src/stdthread.cpp` (subcommands above) + CMakeLists + demo.sh +- S3 write `verify.lua` (A–F hard, G skip-if-present, strace/wchan degrade to SKIP) +- S4 register in `examples/manifest.yaml` (`langs: [cpp]`, `mode: local`, generous timeout) +- S5 capture all transcripts from real runs +- S6 write `_docs/51-std-threading.md` +- S7 2 diagrams + catalogue rows +- S8 gate matrix: verify.lua, runner, validate.py, verbatim-block check, banned words +- S9 PR (per D3, stop there) + +## Acceptance + +`LSP_LANG=cpp lua verify.lua` PASS/FAIL 0 with B and E running for real; runner 1 passed; +`validate.py` OK; every chapter code block a verbatim substring of a real source or a real +transcript; footer states glibc 2.43, the strace method, and the measured futex ranges. + +## Outcome (2026-08-18) + +All steps complete. `LSP_LANG=cpp lua verify.lua` → **PASS 38 / FAIL 0**, with gates B +(futex counts), C (blocking primitives), and E (deadlock + wchan) all running for real. +Runner → 1 passed. `validate.py` → OK. Banned-words clean. **18/18 chapter code blocks +verbatim**; all 12 `Chapter N` cross-references resolve. + +### Correction made before shipping: ch51 was re-deriving ch25 + +Late in drafting, a cross-reference check surfaced that **`_docs/25-shared-state-and-the-futex.md` +already makes the uncontended-vs-contended futex argument** — same tool (`strace`), same +conclusion, with measured counts scaling 5,186 → 488,818 and "~98% of lock ops stay in +userspace" stated in its Figure 25.2. ch51's original opening presented that as its own +central discovery. + +Reframed rather than cut, because the measurement is still needed as the chapter's +foundation and the numbers here are its own. ch51 now opens by citing ch25's result and +naming three things ch25 does **not** cover: + +1. **The strength of the claim.** ch25's uncontended run still made thousands of futex + calls (a workqueue with condvars genuinely blocks). Strip the blocking and subtract a + control and the figure is not 98% but **100%** — 1 futex call, identical to a no-mutex + baseline, whole profile 61 syscalls of loader startup. The baseline-subtraction + technique is what makes the stronger claim measurable. +2. **The C++20 family** — `latch`, `barrier`, `counting_semaphore`, `atomic::wait` all + reaching the same syscall. ch25 used only mutex + condvar. +3. **What the library adds over the futex** — `scoped_lock`'s deadlock avoidance and + `stop_token`, neither visible at the syscall layer. + +The "Cross-check" section now presents the two chapters as independent confirmation +(different examples, different techniques, same structure) rather than a fresh finding. + +### Other deltas + +- **Futex op breakdown added.** `strace -f -e trace=futex | grep -oE "FUTEX_[A-Z_]+"` + shows the contended run is 877 `FUTEX_WAKE_PRIVATE` + 554 `FUTEX_WAIT_PRIVATE`, so + only **554 of 200,000 acquisitions actually slept** (99.7% stayed in user space). This + replaced a softer "roughly 99%" estimate that had been inferred from the total. +- **strace version corrected** in the footer: 7.1, not the 6.19 first written. +- **Manifest timeout 900**, not the Part 14 default 600 — two deliberate deadlocks under + `timeout` (5s + 6s) on top of ~15 straced runs. +- **wchan read per-file, not `cat`-ed together.** `/proc//wchan` has no trailing + newline, so the first version produced `futex_do_waitfutex_do_waitfutex_do_wait`. diff --git a/assets/diagrams/51-deadlock-and-scoped-lock.excalidraw b/assets/diagrams/51-deadlock-and-scoped-lock.excalidraw new file mode 100644 index 0000000..23c7074 --- /dev/null +++ b/assets/diagrams/51-deadlock-and-scoped-lock.excalidraw @@ -0,0 +1,1417 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r951506401", + "type": "rectangle", + "x": 24, + "y": 54, + "width": 1112, + "height": 250, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fdf7ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1696864327, + "versionNonce": 882009688, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1298369681", + "type": "text", + "x": 36, + "y": 60, + "width": 382.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 624673641, + "versionNonce": 630161044, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::lock_guard in opposing orders \u2014 the textbook deadlock", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::lock_guard in opposing orders \u2014 the textbook deadlock", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r866135361", + "type": "rectangle", + "x": 24, + "y": 322, + "width": 1112, + "height": 190, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 647221633, + "versionNonce": 1070921296, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1105811942", + "type": "text", + "x": 36, + "y": 328, + "width": 442.20000000000005, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 63411541, + "versionNonce": 721561615, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::scoped_lock \u2014 takes them all at once, so order stops mattering", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::scoped_lock \u2014 takes them all at once, so order stops mattering", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1059057842", + "type": "rectangle", + "x": 410, + "y": 14, + "width": 340, + "height": 30, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 167390907, + "versionNonce": 1414015864, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t598749153", + "type": "text", + "x": 420, + "y": 22, + "width": 292.6, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1217509602, + "versionNonce": 1728827197, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "two mutexes, two threads, two outcomes", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "two mutexes, two threads, two outcomes", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r1088236309", + "type": "rectangle", + "x": 56, + "y": 96, + "width": 270, + "height": 110, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1527605787, + "versionNonce": 732703036, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t962577298", + "type": "text", + "x": 66, + "y": 104, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 516247331, + "versionNonce": 1607612323, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "thread 1", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "thread 1", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t289461956", + "type": "text", + "x": 66, + "y": 124, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 558739057, + "versionNonce": 212044599, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "lock_guard x(a);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "lock_guard x(a);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t532958755", + "type": "text", + "x": 66, + "y": 139, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1636574218, + "versionNonce": 1783085402, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "lock_guard y(b);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "lock_guard y(b);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r795077280", + "type": "rectangle", + "x": 366, + "y": 96, + "width": 270, + "height": 110, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 339795449, + "versionNonce": 635525130, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t264026042", + "type": "text", + "x": 376, + "y": 104, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1093139175, + "versionNonce": 574210791, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "thread 2", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "thread 2", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t669821858", + "type": "text", + "x": 376, + "y": 124, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 629485935, + "versionNonce": 1935715623, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "lock_guard x(b);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "lock_guard x(b);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t320645199", + "type": "text", + "x": 376, + "y": 139, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 985374562, + "versionNonce": 685473552, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "lock_guard y(a);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "lock_guard y(a);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t142221186", + "type": "text", + "x": 376, + "y": 154, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1194152496, + "versionNonce": 1091128913, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the opposite order", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the opposite order", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1838336578", + "type": "rectangle", + "x": 676, + "y": 96, + "width": 430, + "height": 110, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 208238805, + "versionNonce": 342740078, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t272543360", + "type": "text", + "x": 686, + "y": 104, + "width": 238.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1996802397, + "versionNonce": 426919135, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "each holds what the other needs", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "each holds what the other needs", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1615709274", + "type": "text", + "x": 686, + "y": 124, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 758523452, + "versionNonce": 1714388106, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "timeout(1) exit 124 \u2014 it never returns", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "timeout(1) exit 124 \u2014 it never returns", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t732971893", + "type": "text", + "x": 686, + "y": 139, + "width": 223.85000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1869668504, + "versionNonce": 1513189907, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no error, no exception, no diagnostic", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no error, no exception, no diagnostic", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r313754692", + "type": "rectangle", + "x": 366, + "y": 226, + "width": 740, + "height": 60, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 16829440, + "versionNonce": 505054205, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1343384824", + "type": "text", + "x": 376, + "y": 234, + "width": 377.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 835206980, + "versionNonce": 1253003899, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "and the kernel names it: /proc//task/*/wchan", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "and the kernel names it: /proc//task/*/wchan", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t566770847", + "type": "text", + "x": 376, + "y": 254, + "width": 375.1, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1686599236, + "versionNonce": 1208887505, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tid 2744896 \u00b7 2744897 \u00b7 2744898 \u2192 all three read futex_do_wait", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tid 2744896 \u00b7 2744897 \u00b7 2744898 \u2192 all three read futex_do_wait", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1230755148", + "type": "rectangle", + "x": 56, + "y": 362, + "width": 270, + "height": 110, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1401893573, + "versionNonce": 1649623164, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1832739854", + "type": "text", + "x": 66, + "y": 370, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 216317010, + "versionNonce": 1798641093, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "thread 1", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "thread 1", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t428903591", + "type": "text", + "x": 66, + "y": 390, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1456991440, + "versionNonce": 26877161, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "scoped_lock lock(a, b);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "scoped_lock lock(a, b);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r133285954", + "type": "rectangle", + "x": 366, + "y": 362, + "width": 270, + "height": 110, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1168870666, + "versionNonce": 1229204634, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1098504029", + "type": "text", + "x": 376, + "y": 370, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1213802747, + "versionNonce": 1378551670, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "thread 2", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "thread 2", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t688901206", + "type": "text", + "x": 376, + "y": 390, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 583692033, + "versionNonce": 773048657, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "scoped_lock lock(b, a);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "scoped_lock lock(b, a);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1999858686", + "type": "text", + "x": 376, + "y": 405, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 119902436, + "versionNonce": 1781980688, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "still the opposite order", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "still the opposite order", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1660694315", + "type": "rectangle", + "x": 676, + "y": 362, + "width": 430, + "height": 110, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1433354902, + "versionNonce": 607755322, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t12372128", + "type": "text", + "x": 686, + "y": 370, + "width": 346.5, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1175601003, + "versionNonce": 1287119384, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "try-all, back off, retry \u2014 not argument order", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "try-all, back off, retry \u2014 not argument order", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t87138056", + "type": "text", + "x": 686, + "y": 390, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 46649612, + "versionNonce": 200407006, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "100,000 acquisitions completed, exit 0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "100,000 acquisitions completed, exit 0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t408462342", + "type": "text", + "x": 686, + "y": 405, + "width": 235.95000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 886764729, + "versionNonce": 305227925, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the property lock_guard cannot give you", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the property lock_guard cannot give you", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a52967956", + "type": "arrow", + "x": 326, + "y": 151, + "width": 40, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 924935227, + "versionNonce": 699452423, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a904801729", + "type": "arrow", + "x": 636, + "y": 151, + "width": 40, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 924081186, + "versionNonce": 597362252, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a511984835", + "type": "arrow", + "x": 891, + "y": 206, + "width": 0, + "height": 20, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1636726845, + "versionNonce": 193833657, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 20 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a583219988", + "type": "arrow", + "x": 326, + "y": 417, + "width": 40, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1666686970, + "versionNonce": 1036347708, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a631136553", + "type": "arrow", + "x": 636, + "y": 417, + "width": 40, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1781679209, + "versionNonce": 618024247, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1648573862", + "type": "text", + "x": 40, + "y": 534, + "width": 1089.0, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 967905532, + "versionNonce": 433344177, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Both rows are real runs on this host. The deadlock is reproduced under timeout(1) and hard-gated, including the wchan reading \u2014 the abstraction's failure mode, named by the kernel.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Both rows are real runs on this host. The deadlock is reproduced under timeout(1) and hard-gated, including the wchan reading \u2014 the abstraction's failure mode, named by the kernel.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1660911551", + "type": "text", + "x": 40, + "y": 558, + "width": 738.1, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 156866018, + "versionNonce": 1252168336, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Three threads park, not two: the two workers plus main, blocked in join(). All of them end up in the same kernel function.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Three threads park, not two: the two workers plus main, blocked in join(). All of them end up in the same kernel function.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t545156334", + "type": "text", + "x": 40, + "y": 582, + "width": 780.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1423604633, + "versionNonce": 1191106248, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "scoped_lock is not lock_guard with more arguments. It runs a deadlock-avoidance algorithm, which is why opposing orders are safe.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "scoped_lock is not lock_guard with more arguments. It runs a deadlock-avoidance algorithm, which is why opposing orders are safe.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/51-deadlock-and-scoped-lock.svg b/assets/diagrams/51-deadlock-and-scoped-lock.svg new file mode 100644 index 0000000..9c3cfaa --- /dev/null +++ b/assets/diagrams/51-deadlock-and-scoped-lock.svg @@ -0,0 +1,44 @@ + + + +std::lock_guard in opposing orders — the textbook deadlock + +std::scoped_lock — takes them all at once, so order stops mattering + +two mutexes, two threads, two outcomes + +thread 1 +lock_guard x(a); +lock_guard y(b); + +thread 2 +lock_guard x(b); +lock_guard y(a); +the opposite order + +each holds what the other needs +timeout(1) exit 124 — it never returns +no error, no exception, no diagnostic + +and the kernel names it: /proc/<pid>/task/*/wchan +tid 2744896 · 2744897 · 2744898 → all three read futex_do_wait + +thread 1 +scoped_lock lock(a, b); + +thread 2 +scoped_lock lock(b, a); +still the opposite order + +try-all, back off, retry — not argument order +100,000 acquisitions completed, exit 0 +the property lock_guard cannot give you + + + + + +Both rows are real runs on this host. The deadlock is reproduced under timeout(1) and hard-gated, including the wchan reading — the abstraction's failure mode, named by the kernel. +Three threads park, not two: the two workers plus main, blocked in join(). All of them end up in the same kernel function. +scoped_lock is not lock_guard with more arguments. It runs a deadlock-avoidance algorithm, which is why opposing orders are safe. + \ No newline at end of file diff --git a/assets/diagrams/51-futex-cost-model.excalidraw b/assets/diagrams/51-futex-cost-model.excalidraw new file mode 100644 index 0000000..ff7417d --- /dev/null +++ b/assets/diagrams/51-futex-cost-model.excalidraw @@ -0,0 +1,1723 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r198626223", + "type": "rectangle", + "x": 24, + "y": 54, + "width": 1112, + "height": 210, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1295583756, + "versionNonce": 1340239790, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1510078682", + "type": "text", + "x": 36, + "y": 60, + "width": 488.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1045906124, + "versionNonce": 829675811, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "User space \u2014 atomic compare-exchange on the futex word, no kernel involved", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User space \u2014 atomic compare-exchange on the futex word, no kernel involved", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r309127659", + "type": "rectangle", + "x": 24, + "y": 282, + "width": 1112, + "height": 232, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fdf7ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1602235439, + "versionNonce": 1768930762, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1310271559", + "type": "text", + "x": 36, + "y": 288, + "width": 323.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 106078988, + "versionNonce": 614599115, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Kernel \u2014 futex(2), the syscall ch25 built by hand", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Kernel \u2014 futex(2), the syscall ch25 built by hand", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1295623887", + "type": "rectangle", + "x": 400, + "y": 14, + "width": 360, + "height": 30, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 32284367, + "versionNonce": 1751958688, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t202470435", + "type": "text", + "x": 410, + "y": 22, + "width": 269.5, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1273830773, + "versionNonce": 1623329521, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "200,000 lock/unlock pairs, two ways", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "200,000 lock/unlock pairs, two ways", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r423100809", + "type": "rectangle", + "x": 56, + "y": 96, + "width": 300, + "height": 140, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 414585632, + "versionNonce": 334229532, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t9664564", + "type": "text", + "x": 66, + "y": 104, + "width": 146.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 712610710, + "versionNonce": 937040287, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "baseline \u2014 no mutex", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "baseline \u2014 no mutex", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t917037962", + "type": "text", + "x": 66, + "y": 124, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1984985166, + "versionNonce": 416681943, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "200,000 plain increments", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "200,000 plain increments", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t288393529", + "type": "text", + "x": 66, + "y": 139, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1146199392, + "versionNonce": 1908326128, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the control", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the control", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1750621188", + "type": "text", + "x": 66, + "y": 154, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1215669213, + "versionNonce": 33689886, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex calls: 1", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex calls: 1", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1888950420", + "type": "text", + "x": 66, + "y": 169, + "width": 205.70000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 121983329, + "versionNonce": 1599169415, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "(that 1 is the loader, not a lock)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(that 1 is the loader, not a lock)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r860831958", + "type": "rectangle", + "x": 400, + "y": 96, + "width": 330, + "height": 140, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 141608320, + "versionNonce": 396237003, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t755988890", + "type": "text", + "x": 410, + "y": 104, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 52728044, + "versionNonce": 1196846118, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uncontended \u2014 1 thread", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uncontended \u2014 1 thread", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t964391063", + "type": "text", + "x": 410, + "y": 124, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1018717259, + "versionNonce": 35849415, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "200,000 lock/unlock pairs", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "200,000 lock/unlock pairs", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t118492451", + "type": "text", + "x": 410, + "y": 139, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 486427218, + "versionNonce": 1778887130, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "CAS succeeds every time", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "CAS succeeds every time", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1216114198", + "type": "text", + "x": 410, + "y": 154, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1221660986, + "versionNonce": 909569749, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex calls: 1", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex calls: 1", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1594375079", + "type": "text", + "x": 410, + "y": 169, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 554406485, + "versionNonce": 1391863789, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "IDENTICAL to the baseline", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "IDENTICAL to the baseline", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r963914010", + "type": "rectangle", + "x": 776, + "y": 96, + "width": 330, + "height": 140, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1569084073, + "versionNonce": 252413508, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1725373275", + "type": "text", + "x": 786, + "y": 104, + "width": 192.50000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1106694195, + "versionNonce": 1073594842, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "whole syscall profile: 61", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "whole syscall profile: 61", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t563223493", + "type": "text", + "x": 786, + "y": 124, + "width": 163.35000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 68490480, + "versionNonce": 1401477217, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "execve, mmap, openat, read,", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "execve, mmap, openat, read,", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t770170648", + "type": "text", + "x": 786, + "y": 139, + "width": 163.35000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 184999917, + "versionNonce": 1653878913, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "close \u2014 the dynamic loader.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "close \u2014 the dynamic loader.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1842789387", + "type": "text", + "x": 786, + "y": 154, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1133606767, + "versionNonce": 1006768569, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "The 200,000 pairs contributed", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "The 200,000 pairs contributed", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t69024326", + "type": "text", + "x": 786, + "y": 169, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 39741262, + "versionNonce": 1288665654, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "nothing to it.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "nothing to it.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r134894549", + "type": "rectangle", + "x": 56, + "y": 324, + "width": 330, + "height": 140, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 512541691, + "versionNonce": 1952656155, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t752177662", + "type": "text", + "x": 66, + "y": 332, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1066079749, + "versionNonce": 740613583, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "contended \u2014 8 threads", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "contended \u2014 8 threads", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1262690541", + "type": "text", + "x": 66, + "y": 352, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1494110425, + "versionNonce": 1624614546, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the SAME 200,000 pairs", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the SAME 200,000 pairs", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t953169562", + "type": "text", + "x": 66, + "y": 367, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 372215605, + "versionNonce": 751763361, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "CAS fails often \u2192 sleep + wake", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "CAS fails often \u2192 sleep + wake", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t644827794", + "type": "text", + "x": 66, + "y": 382, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1872823142, + "versionNonce": 317244271, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex calls: 1844\u20133111", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex calls: 1844\u20133111", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t520666506", + "type": "text", + "x": 66, + "y": 397, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 674338934, + "versionNonce": 242145789, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "(5 runs; magnitude varies)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(5 runs; magnitude varies)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1672668707", + "type": "rectangle", + "x": 430, + "y": 324, + "width": 300, + "height": 140, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1429777445, + "versionNonce": 487643918, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1237407364", + "type": "text", + "x": 440, + "y": 332, + "width": 177.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1356225599, + "versionNonce": 801448732, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the blocking primitives", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the blocking primitives", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t538981330", + "type": "text", + "x": 440, + "y": 352, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1580852275, + "versionNonce": 399755890, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "condition_variable \u00b7 latch", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "condition_variable \u00b7 latch", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1210361758", + "type": "text", + "x": 440, + "y": 367, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 401540050, + "versionNonce": 1509984271, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "barrier \u00b7 counting_semaphore", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "barrier \u00b7 counting_semaphore", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1937123880", + "type": "text", + "x": 440, + "y": 382, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 484620141, + "versionNonce": 1946481851, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "atomic::wait", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "atomic::wait", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1989027082", + "type": "text", + "x": 440, + "y": 397, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 728795514, + "versionNonce": 445737299, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "4\u20136 futex calls each", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "4\u20136 futex calls each", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1928851967", + "type": "rectangle", + "x": 776, + "y": 324, + "width": 330, + "height": 140, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1959223605, + "versionNonce": 366699262, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t56120736", + "type": "text", + "x": 786, + "y": 332, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1652738744, + "versionNonce": 117241376, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "five type names,", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "five type names,", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1592796768", + "type": "text", + "x": 786, + "y": 352, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 480895955, + "versionNonce": 1140910611, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one mechanism.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one mechanism.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1705374005", + "type": "text", + "x": 786, + "y": 367, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 58661027, + "versionNonce": 1134529506, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "A thread that must sleep until", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "A thread that must sleep until", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1745442927", + "type": "text", + "x": 786, + "y": 382, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1377307168, + "versionNonce": 835804940, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "further notice has to tell the", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "further notice has to tell the", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t210085027", + "type": "text", + "x": 786, + "y": 397, + "width": 187.55, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 831895034, + "versionNonce": 1071998174, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel \u2014 there is no other way.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel \u2014 there is no other way.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1410603169", + "type": "arrow", + "x": 565, + "y": 236, + "width": 344, + "height": 88, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1291988044, + "versionNonce": 443306203, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -344, + 88 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1311714665", + "type": "text", + "x": 393.0, + "y": 266.0, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1697084358, + "versionNonce": 445509051, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "add contention", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "add contention", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a514350731", + "type": "arrow", + "x": 730, + "y": 394, + "width": 46, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1633736079, + "versionNonce": 935443043, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 46, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1722247229", + "type": "arrow", + "x": 356, + "y": 166, + "width": 44, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 27175993, + "versionNonce": 656801688, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 44, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1207876797", + "type": "text", + "x": 40, + "y": 536, + "width": 877.2500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1481770425, + "versionNonce": 1467040793, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Same total work in all three cost cases \u2014 200,000 increments \u2014 so only the contention differs, which is what makes the syscall counts comparable.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Same total work in all three cost cases \u2014 200,000 increments \u2014 so only the contention differs, which is what makes the syscall counts comparable.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1849645370", + "type": "text", + "x": 40, + "y": 560, + "width": 859.1, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 472723429, + "versionNonce": 1922547739, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "An uncontended std::mutex costs no syscalls at all. What costs is contention: identical work, two orders of magnitude more kernel involvement.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "An uncontended std::mutex costs no syscalls at all. What costs is contention: identical work, two orders of magnitude more kernel involvement.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t776068985", + "type": "text", + "x": 40, + "y": 584, + "width": 816.7500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 889946580, + "versionNonce": 1866458483, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Counted from outside with strace -f -c -e trace=futex. Nothing here is a duration; only the SIGN of the comparison is gated (see ch39).", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Counted from outside with strace -f -c -e trace=futex. Nothing here is a duration; only the SIGN of the comparison is gated (see ch39).", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1691429791", + "type": "text", + "x": 40, + "y": 608, + "width": 393.25000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1410593172, + "versionNonce": 1738440789, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Measured on the Fedora 44 reference host, glibc 2.43, GCC 16.1.1.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Measured on the Fedora 44 reference host, glibc 2.43, GCC 16.1.1.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/51-futex-cost-model.svg b/assets/diagrams/51-futex-cost-model.svg new file mode 100644 index 0000000..b2fe1f9 --- /dev/null +++ b/assets/diagrams/51-futex-cost-model.svg @@ -0,0 +1,54 @@ + + + +User space — atomic compare-exchange on the futex word, no kernel involved + +Kernel — futex(2), the syscall ch25 built by hand + +200,000 lock/unlock pairs, two ways + +baseline — no mutex +200,000 plain increments +the control +futex calls: 1 +(that 1 is the loader, not a lock) + +uncontended — 1 thread +200,000 lock/unlock pairs +CAS succeeds every time +futex calls: 1 +IDENTICAL to the baseline + +whole syscall profile: 61 +execve, mmap, openat, read, +close — the dynamic loader. +The 200,000 pairs contributed +nothing to it. + +contended — 8 threads +the SAME 200,000 pairs +CAS fails often → sleep + wake +futex calls: 1844–3111 +(5 runs; magnitude varies) + +the blocking primitives +condition_variable · latch +barrier · counting_semaphore +atomic::wait +4–6 futex calls each + +five type names, +one mechanism. +A thread that must sleep until +further notice has to tell the +kernel — there is no other way. + +add contention +add contention + + +Same total work in all three cost cases — 200,000 increments — so only the contention differs, which is what makes the syscall counts comparable. +An uncontended std::mutex costs no syscalls at all. What costs is contention: identical work, two orders of magnitude more kernel involvement. +Counted from outside with strace -f -c -e trace=futex. Nothing here is a duration; only the SIGN of the comparison is gated (see ch39). +Measured on the Fedora 44 reference host, glibc 2.43, GCC 16.1.1. + \ No newline at end of file diff --git a/assets/diagrams/README.md b/assets/diagrams/README.md index b2304a0..d09d94b 100644 --- a/assets/diagrams/README.md +++ b/assets/diagrams/README.md @@ -99,3 +99,5 @@ here as you add them: | `49-execution-model-lanes` | 49 | ch49's C++26 support split by what was measured on this toolchain -- the amber live lane (Contracts P2900 built four times for ignore/observe/enforce/quick_enforce, and safety hardening where Fedora's `_GLIBCXX_ASSERTIONS` is on at `-O0` and off at `-O2`, gated statically on the unchecked side because gating on UB output would be unsound) beside the dashed forward-looking lane (P2300 senders with `__cpp_lib_senders` undefined and `` still the C++17 header, P2996 reflection undefined in GCC 16 and rejected by clang 22), both lanes labelled measured-not-inferred | | `50-thread-two-identities` | 50 | one thread seen twice -- an upper C++ band (`std::thread` with an opaque `thread::id` and no name/stack/affinity control, the amber `native_handle()` escape hatch whose `native_handle_type` is `pthread_t` by `static_assert`, and `std::jthread`/`stop_token` as the cooperative answer) over a lower Linux band (`pthread_t` as an opaque handle you may only compare, the amber `gettid()` kernel task id that names a `/proc/self/task//` directory and equals the pid for the main thread, plus the four controls: `pthread_setname_np` into procfs `comm` with 16 chars refused as `ERANGE`, `pthread_attr_setstacksize` giving 262144 with sub-`PTHREAD_STACK_MIN` refused as `EINVAL`, per-thread `pthread_setaffinity_np`, and `SCHED_OTHER`/0 with `SCHED_FIFO` refused as `EPERM`), every facet converging on `digest=0x481984990deee5ff` | | `50-cancellation-two-roads` | 50 | why C++20 chose cooperative cancellation -- an amber upper band walking `pthread_cancel` from a worker parked on a cancellation point through glibc's forced unwind to the result that RAII really runs (`~Guard()`, the `pthread_cleanup_push` handler, and `join` reporting `PTHREAD_CANCELED`), with a dashed ghost box for the trap where `catch (...)` without a rethrow yields `FATAL: exception not rethrown` and SIGABRT/134; and a lower band showing `request_stop()` setting a flag that `stop_token::stop_requested()` polls, so the target picks its own safe stopping point with no unwind and nothing a catch-all can swallow | +| `51-futex-cost-model` | 51 | the cost of a standard lock, measured -- an upper user-space band (a no-mutex baseline at 1 futex call, the amber uncontended case doing 200,000 lock/unlock pairs for the *same* 1 call, and a note that the whole run's syscall profile is 61 calls of `execve`/`mmap`/`openat` loader startup to which the 200,000 pairs contributed nothing) over a lower amber kernel band (the identical 200,000 pairs contended across 8 threads costing 1844-3111 futex calls, the blocking primitives `condition_variable`/`latch`/`barrier`/`counting_semaphore`/`atomic::wait` at 4-6 calls each, and a note that five type names are one mechanism); counted from outside with `strace -f -c -e trace=futex`, sign gated and magnitude never | +| `51-deadlock-and-scoped-lock` | 51 | two mutexes, two threads, two outcomes -- an amber upper band walking two `std::lock_guard` threads acquiring `a,b` and `b,a` into a mutual hold where `timeout(1)` exits 124 with no error or diagnostic, and a dashed box showing the kernel naming it via `/proc//task/*/wchan` reading `futex_do_wait` on all three tids (both workers plus main in `join`); and a lower band where `std::scoped_lock(a, b)` and `scoped_lock(b, a)` in the same opposing orders complete 100,000 acquisitions because scoped_lock runs a try-all/back-off/retry algorithm rather than locking in argument order | diff --git a/examples/51-std-threading/README.md b/examples/51-std-threading/README.md new file mode 100644 index 0000000..d9c7efd --- /dev/null +++ b/examples/51-std-threading/README.md @@ -0,0 +1,171 @@ +# 51 — the standard threading library, and what it costs + +Chapter 25 already showed that a mutex is a futex whose uncontended fast path +stays in user space, measuring ~98% of acquisitions never reaching the kernel. +This example starts there and pushes on three things ch25 did not cover: + +- **how far "nearly free" goes** — with the blocking stripped away and a control + to subtract, an uncontended `std::mutex` costs *zero* syscalls, not merely few +- **the family** — `condition_variable`, `latch`, `barrier`, + `counting_semaphore`, and `atomic::wait` are one mechanism, not five +- **what the library adds over the futex** — `std::scoped_lock` will not + deadlock where `lock_guard` will, and `std::stop_token` answers ch50's + cancellation problem + +## The headline + +Three cases, all performing exactly the same 200,000 increments — only the +contention differs: + +``` +[host]$ for c in baseline uncontended contended; do + printf "%-12s futex=%s\n" "$c" \ + "$(strace -f -c -e trace=futex cpp/build/release/stdthread $c 2>&1 | awk '/futex/{print $4}')" + done +baseline futex=1 +uncontended futex=1 +contended futex=2360 +``` + +`baseline` uses no mutex at all, and it still reports 1 — that call is the +dynamic loader, not a lock. `uncontended` performs 200,000 `lock_guard` +acquisitions and reports **the same 1**. Its entire syscall profile is 61 +calls, all `execve`/`mmap`/`openat`/`read`/`close`: + +``` +[host]$ strace -f -c cpp/build/release/stdthread uncontended 2>&1 | tail -3 + 0.00 0.000000 0 1 execve +------ ----------- ----------- --------- --------- ---------------- +100.00 0.000084 1 61 1 total +``` + +An uncontended `std::mutex` is an atomic compare-exchange in user space. The +cost people attribute to "locking" is really the cost of **contention**. + +## The subcommands + +| subcommand | what it measures | futex calls (3 runs) | +| --- | --- | --- | +| `baseline` | 200,000 increments, no mutex — the control | 1, 1, 1 | +| `uncontended` | the same 200,000, each under a `lock_guard`, one thread | 1, 1, 1 | +| `contended` | the same 200,000, split across 8 threads | 2360, 2199, 2469 | +| `condvar` | `condition_variable::wait` with a predicate | 5, 5, 5 | +| `latch` | `std::latch::arrive_and_wait` (single-use) | 5, 8, 5 | +| `barrier` | `std::barrier` (reusable, with a completion function) | 5, 4, 6 | +| `semaphore` | `std::counting_semaphore::acquire` | 4, 4, 4 | +| `atomic-wait` | `std::atomic::wait` / `notify_one` (C++20) | 4, 4, 4 | +| `stoptoken` | `jthread` + `request_stop()` — the ch50 contrast | 2, 2, 2 | +| `deadlock-safe` | `scoped_lock(a,b)` vs `scoped_lock(b,a)`, 100,000 acquisitions | — | +| `deadlock-naive` | the same with `lock_guard` — **hangs on purpose** | — | + +Five different type names for the blocking primitives, one mechanism +underneath. A thread that must sleep until further notice has to tell the +kernel; there is no other way to do it. + +Every non-hanging subcommand prints `digest=0x481984990deee5ff` over the payload +`The quick brown.` — the value ch46's C++, ch47's Go, ch48's Rust, ch49's +`conc`, and ch50's `pthreads` all produce. + +## Deadlock, both directions + +``` +[host]$ ./demo.sh cpp run deadlock-safe +deadlock-safe: completed 100000 acquisitions in opposing orders + +[host]$ timeout 5 cpp/build/release/stdthread deadlock-naive; echo "exit=$?" +deadlock-naive: two lock_guards in opposing orders; this will hang +exit=124 +``` + +`std::scoped_lock` takes all its mutexes at once with a deadlock-avoidance +algorithm — try, back off, retry — rather than locking them in argument order. +So `scoped_lock(a, b)` on one thread and `scoped_lock(b, a)` on the other is +safe. `lock_guard` in those same orders is the textbook deadlock. + +And the kernel will tell you exactly what happened. While the naive version +hangs: + +``` +[host]$ p=$(pgrep -x stdthread) +[host]$ for t in /proc/$p/task/*; do + printf "tid=%-8s state=%-2s wchan=%s\n" "$(basename $t)" \ + "$(awk '/^State:/{print $2}' $t/status)" "$(cat $t/wchan)" + done +tid=2744896 state=S wchan=futex_do_wait +tid=2744897 state=S wchan=futex_do_wait +tid=2744898 state=S wchan=futex_do_wait +``` + +Three threads, not two: both workers plus the main thread blocked in `join()`. +All of them parked in the same kernel function, readable unprivileged. + +**`deadlock-naive` never returns.** It is its own subcommand for the same reason +ch50's `cancel-swallow` was, and `verify.lua` only ever invokes it under +`timeout(1)`. + +## Layout + +``` +51-std-threading/ +├── demo.sh # dispatcher (C++ only) +├── verify.lua # gates A-F hard, G skip-if-present +└── cpp/ + ├── CMakeLists.txt + ├── CMakePresets.json + ├── demo.sh + └── src/stdthread.cpp +``` + +C++-only, like ch49 and ch50. Standard library only — no Boost, no Conan, no +network, no VM, no root. + +## Build and run + +``` +[host]$ ./demo.sh cpp build +[host]$ ./demo.sh cpp run uncontended +[host]$ ./demo.sh cpp run contended +[host]$ timeout 5 cpp/build/release/stdthread deadlock-naive # hangs by design +``` + +Bare `./demo.sh` builds and runs `baseline`. To count syscalls yourself, wrap +any subcommand: + +``` +[host]$ strace -f -c -e trace=futex cpp/build/release/stdthread contended +``` + +## Verification + +``` +[host]$ LSP_LANG=cpp lua verify.lua +``` + +Gates A–F are hard: + +- **A** — every non-hanging subcommand computes the expected digest, and the + three cost cases each report `acc=200000` so their syscall counts are + comparable in the first place. +- **B** — the headline: `uncontended` costs no more futex calls than the + no-mutex `baseline`, while `contended` — the identical work — costs hundreds, + more than two orders of magnitude more. +- **C** — `condvar`, `latch`, `barrier`, `semaphore`, and `atomic-wait` each + issue at least one futex call. +- **D** — `deadlock-safe` completes 100,000 acquisitions in opposing orders. +- **E** — `deadlock-naive` times out (exit 124), and while hung every task's + `wchan` reads a futex wait. +- **F** — `stoptoken`'s worker returns normally after `request_stop()`: no + `PTHREAD_CANCELED`, no forced unwind. + +**G** (clang parity) runs if `clang++` is present, otherwise prints an +informational `SKIP`. + +Two gates degrade to a `SKIP` rather than a FAIL if the host will not +cooperate: if `strace` cannot attach (ptrace hardening), gates B and C are +skipped; if `/proc//wchan` is unreadable, the wchan half of gate E is. +Neither ever becomes a false PASS. + +**Not gated:** the absolute futex count for the contended case. It is +scheduling-dependent — 1844–3111 observed across runs on the reference host — +so only the *sign* of the comparison is asserted, per ch39. `verify.lua` prints +a line saying so. diff --git a/examples/51-std-threading/cpp/.clang-format b/examples/51-std-threading/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/51-std-threading/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/51-std-threading/cpp/.clang-tidy b/examples/51-std-threading/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/51-std-threading/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/51-std-threading/cpp/CMakeLists.txt b/examples/51-std-threading/cpp/CMakeLists.txt new file mode 100644 index 0000000..f50a764 --- /dev/null +++ b/examples/51-std-threading/cpp/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.25) +project(stdthread CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +# No optimization games here: the whole chapter is about which syscalls the +# standard primitives issue, and that does not change with -O level. But the +# increment loops must not be deleted -- every one of them writes to a +# variable that report() prints, which is what keeps them alive. +add_executable(stdthread src/stdthread.cpp) +target_link_libraries(stdthread PRIVATE Threads::Threads) diff --git a/examples/51-std-threading/cpp/CMakePresets.json b/examples/51-std-threading/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/51-std-threading/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/51-std-threading/cpp/demo.sh b/examples/51-std-threading/cpp/demo.sh new file mode 100755 index 0000000..8377949 --- /dev/null +++ b/examples/51-std-threading/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/stdthread + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run baseline + ;; + *) + echo "usage: $0 [build|run ]" >&2 + exit 2 + ;; +esac diff --git a/examples/51-std-threading/cpp/src/stdthread.cpp b/examples/51-std-threading/cpp/src/stdthread.cpp new file mode 100644 index 0000000..ae25278 --- /dev/null +++ b/examples/51-std-threading/cpp/src/stdthread.cpp @@ -0,0 +1,364 @@ +// stdthread -- the reference program for ch51. +// +// ch50 showed what a std::thread IS on Linux (a pthread). This program asks +// what the standard library's SYNCHRONIZATION primitives are, and the answer +// is the same one twice: +// +// when they block -> futex(2), the syscall ch25 built by hand +// when they do not -> nothing at all, not one syscall +// +// The second half is the one people get wrong. An uncontended std::mutex is +// an atomic compare-exchange and no kernel involvement whatsoever; the cost +// people attribute to "locking" is really the cost of CONTENTION. +// +// Every subcommand below is designed to be counted from outside with +// +// strace -f -c -e trace=futex ./stdthread +// +// rather than to time itself. Nothing here reports a duration (see ch39); the +// observable is a syscall count, which is reproducible in sign across hosts +// even where its magnitude is not. +// +// `deadlock-naive` hangs on purpose. It is its own subcommand for the same +// reason ch50's cancel-swallow was: a subcommand that never returns must +// never be able to take the others with it. Run it under timeout(1). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// ch46-ch50's payload ("The quick brown."), carried forward unchanged. +constexpr std::uint8_t kPayload[16] = {0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69, 0x63, + 0x6b, 0x20, 0x62, 0x72, 0x6f, 0x77, 0x6e, 0x2e}; + +constexpr std::uint64_t kFnvOffsetBasis = 0xcbf29ce484222325ULL; +constexpr std::uint64_t kFnvPrime = 0x00000100000001b3ULL; + +std::uint64_t fnv1a(const std::uint8_t* data, std::size_t len) { + std::uint64_t h = kFnvOffsetBasis; + for (std::size_t i = 0; i < len; ++i) { + h ^= data[i]; + h *= kFnvPrime; // uint64_t wraps by definition -- no UB to sanitize + } + return h; +} + +void report(const char* what, long acc) { + std::printf("stdthread report: case=%s acc=%ld digest=0x%016llx\n", what, acc, + static_cast(fnv1a(kPayload, sizeof kPayload))); +} + +// The three cost cases below all perform EXACTLY the same amount of work -- +// kTotalOps increments -- so the only variable between them is how many +// threads are contending for the lock. That is what makes the futex counts +// comparable: same operations, same result, different kernel involvement. +constexpr long kTotalOps = 200000; +constexpr int kContendedThreads = 8; + +// ── baseline: no mutex, no threads ─────────────────────────────────────── +// +// The control. Whatever syscalls this run makes are the floor: process +// startup, the dynamic loader, and nothing else. The uncontended case is +// measured against THIS, not against zero, because a program that has linked +// libstdc++ has already made a handful of syscalls before main() runs. + +void case_baseline() { + long n = 0; + for (long i = 0; i < kTotalOps; ++i) { + ++n; + } + report("baseline", n); +} + +// ── uncontended: one thread, one mutex, 200k lock/unlock pairs ─────────── +// +// std::mutex is a pthread_mutex_t is a futex word. Locking it when it is +// free is a single atomic compare-exchange in user space; glibc only issues +// futex(FUTEX_WAIT) when the exchange fails, which it never does here +// because nobody else is asking. + +void case_uncontended() { + std::mutex m; + long n = 0; + for (long i = 0; i < kTotalOps; ++i) { + std::lock_guard g(m); + ++n; + } + report("uncontended", n); +} + +// ── contended: eight threads, one mutex, the same 200k pairs ───────────── +// +// Identical total work, split across threads that actually collide. Now the +// compare-exchange fails often, and every failure is a trip into the kernel +// to sleep on the futex word plus another to wake a waiter. + +void case_contended() { + std::mutex m; + long n = 0; + std::vector threads; + threads.reserve(kContendedThreads); + for (int t = 0; t < kContendedThreads; ++t) { + threads.emplace_back([&m, &n] { + for (long i = 0; i < kTotalOps / kContendedThreads; ++i) { + std::lock_guard g(m); + ++n; + } + }); + } + threads.clear(); // jthread joins on destruction + report("contended", n); +} + +// ── the blocking primitives ────────────────────────────────────────────── +// +// Each of these makes a thread WAIT for something another thread will do. +// There is no way to do that in user space alone -- a thread that must sleep +// until further notice has to tell the kernel. So each one reaches futex(2), +// and the point of these cases is that they are all the same mechanism +// wearing different type names. +// +// Each spawns a waiter, lets it reach the blocking call, then satisfies it. +// The main thread's brief sleep is scaffolding to make the waiter reach its +// wait before the signal arrives -- it is not being measured, and no +// assertion anywhere depends on its length. + +void settle() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +void case_condvar() { + std::mutex m; + std::condition_variable cv; + bool ready = false; + long n = 0; + + std::jthread waiter([&] { + std::unique_lock lock(m); + // The predicate form, always: a bare wait() may return spuriously, + // and this overload re-checks the condition on every wakeup. + cv.wait(lock, [&ready] { return ready; }); + n = 1; + }); + + settle(); + { + std::lock_guard g(m); + ready = true; + } + cv.notify_one(); + waiter.join(); + report("condvar", n); +} + +void case_latch() { + // A latch is single-use: count down to zero and it stays there. + std::latch gate{3}; + std::atomic arrived{0}; + + std::vector waiters; + for (int i = 0; i < 2; ++i) { + waiters.emplace_back([&] { + arrived.fetch_add(1, std::memory_order_relaxed); + gate.arrive_and_wait(); + }); + } + settle(); + gate.count_down(); + waiters.clear(); + report("latch", arrived.load()); +} + +void case_barrier() { + // A barrier is reusable: it resets after each phase, and runs an optional + // completion function on the thread that trips it. + std::atomic phases{0}; + std::barrier sync{2, [&phases]() noexcept { phases.fetch_add(1, std::memory_order_relaxed); }}; + + std::jthread a([&] { + sync.arrive_and_wait(); + sync.arrive_and_wait(); + }); + settle(); + sync.arrive_and_wait(); + sync.arrive_and_wait(); + a.join(); + report("barrier", phases.load()); +} + +void case_semaphore() { + std::counting_semaphore<4> sem{0}; + long n = 0; + std::jthread waiter([&] { + sem.acquire(); + n = 1; + }); + settle(); + sem.release(); + waiter.join(); + report("semaphore", n); +} + +void case_atomic_wait() { + // C++20 gave std::atomic itself wait/notify. On Linux this is the futex + // with the type-system noise removed: wait(old) sleeps while the value is + // still `old`, exactly the FUTEX_WAIT contract from ch25. + std::atomic flag{0}; + long n = 0; + std::jthread waiter([&] { + flag.wait(0); // block while flag == 0 + n = flag.load(); + }); + settle(); + flag.store(7); + flag.notify_one(); + waiter.join(); + report("atomic-wait", n); +} + +// ── stop_token: the ch50 payoff ────────────────────────────────────────── +// +// ch50 showed that pthread_cancel unwinds through an exception a catch-all +// can fatally swallow. This is the alternative C++20 chose: request_stop() +// sets a flag, the target polls it where IT decides is safe, and nothing is +// thrown, unwound, or catchable. The thread stops because it agreed to. + +void case_stoptoken() { + std::atomic loops{0}; + std::jthread worker([&loops](std::stop_token stop) { + while (!stop.stop_requested()) { + loops.fetch_add(1, std::memory_order_relaxed); + std::this_thread::yield(); + } + }); + + settle(); + worker.request_stop(); + worker.join(); + // The worker chose its own stopping point and returned normally. There is + // no PTHREAD_CANCELED here, no forced unwind, and no way for a catch (...) + // to turn this into an abort. + std::printf("stoptoken: stop_requested honored, worker returned normally\n"); + report("stoptoken", loops.load() > 0 ? 1 : 0); +} + +// ── deadlock, both directions ──────────────────────────────────────────── +// +// Two mutexes, two threads, opposing acquisition orders: the textbook +// deadlock. The textbook is right, and the standard library ships the fix. +// +// std::scoped_lock takes all its mutexes at once using a deadlock-avoidance +// algorithm (try, back off, retry) rather than locking them in argument +// order. So scoped_lock(a,b) on one thread and scoped_lock(b,a) on the other +// is SAFE -- the argument order does not matter, which is exactly the +// property lock_guard cannot give you. + +constexpr long kDeadlockIters = 50000; + +void case_deadlock_safe() { + std::mutex a; + std::mutex b; + std::atomic done{0}; + + std::jthread t1([&] { + for (long i = 0; i < kDeadlockIters; ++i) { + std::scoped_lock lock(a, b); + done.fetch_add(1, std::memory_order_relaxed); + } + }); + std::jthread t2([&] { + for (long i = 0; i < kDeadlockIters; ++i) { + // The OPPOSITE order, deliberately. scoped_lock does not care. + std::scoped_lock lock(b, a); + done.fetch_add(1, std::memory_order_relaxed); + } + }); + t1.join(); + t2.join(); + std::printf("deadlock-safe: completed %ld acquisitions in opposing orders\n", done.load()); + report("deadlock-safe", done.load()); +} + +[[noreturn]] void case_deadlock_naive() { + // This never returns. Run it under timeout(1), and while it hangs, read + // /proc//task/*/wchan to see every thread parked in futex_do_wait. + static std::mutex a; + static std::mutex b; + + std::printf("deadlock-naive: two lock_guards in opposing orders; this will hang\n"); + std::fflush(stdout); + + std::thread t1([] { + for (;;) { + std::lock_guard x(a); + std::this_thread::sleep_for(std::chrono::microseconds(1)); + std::lock_guard y(b); + } + }); + std::thread t2([] { + for (;;) { + std::lock_guard x(b); + std::this_thread::sleep_for(std::chrono::microseconds(1)); + std::lock_guard y(a); + } + }); + t1.join(); + t2.join(); + std::abort(); // unreachable: the joins never return +} + +void usage() { + std::fprintf(stderr, + "usage: stdthread \n"); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 2) { + usage(); + return 2; + } + const std::string what = argv[1]; + + if (what == "baseline") { + case_baseline(); + } else if (what == "uncontended") { + case_uncontended(); + } else if (what == "contended") { + case_contended(); + } else if (what == "condvar") { + case_condvar(); + } else if (what == "latch") { + case_latch(); + } else if (what == "barrier") { + case_barrier(); + } else if (what == "semaphore") { + case_semaphore(); + } else if (what == "atomic-wait") { + case_atomic_wait(); + } else if (what == "stoptoken") { + case_stoptoken(); + } else if (what == "deadlock-safe") { + case_deadlock_safe(); + } else if (what == "deadlock-naive") { + case_deadlock_naive(); + } else { + usage(); + return 2; + } + return 0; +} diff --git a/examples/51-std-threading/demo.sh b/examples/51-std-threading/demo.sh new file mode 100755 index 0000000..dfdb907 --- /dev/null +++ b/examples/51-std-threading/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +# Single-language example (C++ only), like ch49 and ch50. This chapter measures +# what the C++ standard library's synchronization primitives cost on this +# kernel; Go and Rust have their own primitives over the same futex, and ch56 +# is where the cross-language comparison belongs. +langs=(cpp) + +case "${1:-all}" in + cpp) + shift + exec "./cpp/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/51-std-threading/verify.lua b/examples/51-std-threading/verify.lua new file mode 100644 index 0000000..fa1f31e --- /dev/null +++ b/examples/51-std-threading/verify.lua @@ -0,0 +1,191 @@ +-- Verify 51-std-threading (C++ only): what the C++ standard library's +-- synchronization primitives become on this kernel, and what they cost. +-- +-- ch50 established that a std::thread IS a pthread here. This chapter asks the +-- same question of the synchronization primitives, and the answer is the same +-- one twice: a futex(2) when they block, and NOTHING AT ALL when they do not. +-- +-- Every assertion below is a syscall COUNT observed from outside the program +-- with `strace -f -c -e trace=futex`, or a string the kernel put in procfs. +-- NOTHING HERE IS TIMED (see ch39). Syscall counts are reproducible in sign +-- across hosts even where their magnitude is not, and only signs are gated. +-- +-- Hard-gated (A-F), all local, unprivileged, offline: +-- A. Every non-hanging subcommand computes the same digest, +-- 0x481984990deee5ff -- ch46's C++, ch47's Go, ch48's Rust, ch49's conc, +-- ch50's pthreads. +-- B. THE HEADLINE. `baseline` (no mutex) and `uncontended` (200,000 +-- lock/unlock pairs) issue the SAME small number of futex calls, while +-- `contended` -- the identical 200,000 pairs, split across 8 threads -- +-- issues hundreds. An uncontended std::mutex costs no syscalls at all; +-- what costs is contention. The gate compares uncontended to baseline +-- rather than asserting zero, because a program that has linked +-- libstdc++ has already made a few syscalls before main() runs. +-- C. The blocking primitives all reach the same syscall: condvar, latch, +-- barrier, semaphore, and atomic-wait each issue >= 1 futex call. They +-- are one mechanism wearing five type names. +-- D. deadlock-safe: std::scoped_lock acquires two mutexes in OPPOSING +-- orders on two threads and completes anyway -- the deadlock-avoidance +-- property std::lock_guard cannot give you. +-- E. deadlock-naive: the same program with lock_guard hangs. Asserted as a +-- timeout (exit 124 from timeout(1)) AND, while it hangs, every task in +-- /proc//task/*/wchan parked in a futex wait. The abstraction's +-- failure mode, named by the kernel. +-- F. stoptoken: the worker returns normally after request_stop(). No +-- PTHREAD_CANCELED, no forced unwind -- the ch50 contrast. +-- +-- Gated-if-present (G): clang++ parity on the digest. +-- +-- Degrades to an informational SKIP, never a FAIL and never a false PASS: +-- * strace unable to attach (ptrace hardening) -> gates B and C skipped +-- * /proc//wchan unreadable -> the wchan half of gate E skipped + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" then + checks.skip("51-std-threading is C++-only (LSP_LANG=" .. tostring(lang) .. ")") +end + +local function lit(s) + return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")) +end + +local function tool_present(cmd) + return checks.run("command -v " .. cmd .. " >/dev/null 2>&1").exit == 0 +end + +local BIN = "cpp/build/release/stdthread" +local EXPECTED_DIGEST = "digest=0x481984990deee5ff" + +-- Count futex syscalls issued by one subcommand, from outside the process. +-- Returns nil if strace could not attach, which is a SKIP rather than a FAIL. +local function futex_count(case) + local r = checks.run("strace -f -c -e trace=futex " .. BIN .. " " .. case .. + " 2>&1 | awk '/futex/{print $4}' | head -1") + local n = tonumber((r.out:gsub("%s", ""))) + return n +end + +local b = checks.run("./demo.sh cpp build") +checks.expect_exit(b, 0, "cpp: builds (stdlib only, no Boost, no Conan, no network)") + +-- ── A. one digest, every non-hanging case ──────────────────────────────── + +local cases = { "baseline", "uncontended", "contended", "condvar", "latch", "barrier", + "semaphore", "atomic-wait", "stoptoken", "deadlock-safe" } +local out = {} +for _, c in ipairs(cases) do + local r = checks.run("./demo.sh cpp run " .. c) + checks.expect_exit(r, 0, "cpp: case " .. c .. " runs") + checks.expect_match(r.out, lit(EXPECTED_DIGEST), + "cpp: case " .. c .. " computes the expected digest (== ch46/47/48/49/50's)") + out[c] = r.out +end + +-- The three cost cases must do the SAME WORK, or their syscall counts are not +-- comparable. Assert that before comparing anything. +checks.expect_match(out["baseline"], "acc=200000", + "cpp: baseline performed 200000 increments") +checks.expect_match(out["uncontended"], "acc=200000", + "cpp: uncontended performed the same 200000 increments, each under a lock") +checks.expect_match(out["contended"], "acc=200000", + "cpp: contended performed the same 200000 increments across 8 threads -- same work, " + .. "so the futex counts below are comparable") + +-- ── B. the headline: locking is free until it is contended ─────────────── + +local have_strace = tool_present("strace") +local n_base = have_strace and futex_count("baseline") or nil + +if not have_strace or n_base == nil then + print("SKIP: strace could not count syscalls (absent or ptrace-restricted) -- " .. + "gates B and C not asserted") +else + local n_unc = futex_count("uncontended") + local n_con = futex_count("contended") + + checks.expect_match(tostring(n_unc ~= nil and n_unc <= n_base + 1), "true", + "cpp: 200000 uncontended lock/unlock pairs cost no more futex calls than the " .. + "no-mutex baseline (baseline=" .. tostring(n_base) .. ", uncontended=" .. + tostring(n_unc) .. ") -- an uncontended std::mutex is an atomic CAS, not a syscall") + checks.expect_match(tostring(n_con ~= nil and n_con > 500), "true", + "cpp: the identical 200000 pairs, contended, cost hundreds of futex calls " .. + "(contended=" .. tostring(n_con) .. ") -- the cost is contention, not locking") + checks.expect_match(tostring(n_con ~= nil and n_unc ~= nil and n_con > n_unc * 100), "true", + "cpp: contended exceeds uncontended by more than two orders of magnitude") + + -- ── C. five type names, one mechanism ────────────────────────────────── + for _, c in ipairs({ "condvar", "latch", "barrier", "semaphore", "atomic-wait" }) do + local n = futex_count(c) + checks.expect_match(tostring(n ~= nil and n >= 1), "true", + "cpp: " .. c .. " blocks, and blocking means futex(2) (calls=" .. tostring(n) .. ")") + end +end + +-- ── D. scoped_lock survives opposing acquisition orders ────────────────── + +checks.expect_match(out["deadlock-safe"], "completed 100000 acquisitions in opposing orders", + "cpp: std::scoped_lock(a,b) and scoped_lock(b,a) on two threads completed -- " .. + "argument order does not matter to scoped_lock") + +-- ── E. the naive version deadlocks, and the kernel says where ──────────── + +local naive = checks.run("timeout 5 " .. BIN .. " deadlock-naive >/dev/null 2>&1; echo exit=$?") +checks.expect_match(naive.out, "exit=124", + "cpp: two lock_guards in opposing orders deadlock -- timeout(1) had to kill it") + +-- Catch it in the act and ask the kernel what each thread is doing. Bounded +-- polling, never an unbounded wait: if the process does not appear, skip. +local wchan = checks.run([[ + timeout 6 ]] .. BIN .. [[ deadlock-naive >/dev/null 2>&1 & + for i in $(seq 1 50); do pgrep -x stdthread >/dev/null 2>&1 && break; done + sleep 1 + p=$(pgrep -x stdthread | head -1) + if [ -z "$p" ]; then echo NOPROC; else + # /proc//wchan has no trailing newline, so read each one separately + # rather than cat-ing them all into one run-on string. + for t in /proc/$p/task/*; do printf '%s ' "$(cat $t/wchan 2>/dev/null)"; done + fi + wait 2>/dev/null +]]) + +if wchan.out:find("NOPROC") or wchan.out:gsub("%s", "") == "" then + print("SKIP: /proc//wchan unreadable or the process was not caught -- " .. + "the wchan half of gate E not asserted") +else + checks.expect_match(wchan.out, "futex", + "cpp: while deadlocked, procfs reports every thread parked in a futex wait " .. + "(wchan: " .. wchan.out:gsub("%s+", " ") .. ")") +end + +-- ── F. stop_token: cooperative, and it returns normally ────────────────── + +checks.expect_match(out["stoptoken"], "stop_requested honored, worker returned normally", + "cpp: the jthread observed request_stop() and returned on its own terms -- " .. + "no PTHREAD_CANCELED and no forced unwind, unlike ch50's pthread_cancel") + +-- ── G. clang parity (skip-if-absent) ───────────────────────────────────── + +if tool_present("clang++") then + local cl = checks.run( + "cd cpp && cmake --preset release-clang >/dev/null 2>&1 && " .. + "cmake --build --preset release-clang --target stdthread >/dev/null 2>&1 && " .. + "./build/release-clang/stdthread uncontended") + checks.expect_exit(cl, 0, "cpp: clang-built stdthread runs") + checks.expect_match(cl.out, lit(EXPECTED_DIGEST), + "cpp: clang and GCC agree on the digest") +else + print("SKIP: clang++ not found on PATH -- gate G not asserted") +end + +-- ── not gated ──────────────────────────────────────────────────────────── + +print("info: absolute futex counts for the contended case are NOT gated -- they are " .. + "scheduling-dependent (1844-3111 observed across runs on the reference host); only " .. + "the sign of the comparison is asserted") + +checks.finish() diff --git a/examples/manifest.yaml b/examples/manifest.yaml index 3dee585..9f3357b 100644 --- a/examples/manifest.yaml +++ b/examples/manifest.yaml @@ -209,3 +209,9 @@ examples: langs: [cpp] mode: local timeout: 600 + # 51 runs two deliberate deadlocks under timeout(1) (5s and 6s) on top of + # ~15 straced runs, so it needs more headroom than the Part 14 default. + - id: 51-std-threading + langs: [cpp] + mode: local + timeout: 900