diff --git a/_docs/50-pthreads.md b/_docs/50-pthreads.md new file mode 100644 index 0000000..754ac84 --- /dev/null +++ b/_docs/50-pthreads.md @@ -0,0 +1,705 @@ +--- +title: "pthreads: the thread under std::thread, its two identities, and the six controls the standard does not give you" +order: 50 +part: "Compendium: C++ Concurrency" +description: "pthreads runs one facet per subcommand over the per-thread controls POSIX offers and the C++ standard library does not expose: gettid() versus the opaque pthread_t (and why the main thread's tid equals the pid), pthread_setname_np landing in /proc/self/task//comm with a 16-character name refused as ERANGE, pthread_attr_setstacksize giving exactly 262144 against an 8388608 default with sub-PTHREAD_STACK_MIN requests refused as EINVAL, per-thread pthread_setaffinity_np putting two workers on two CPUs while the caller's mask is untouched, SCHED_OTHER at priority 0 with an unprivileged SCHED_FIFO refused as EPERM, and pthread_cancel unwinding C++ destructors -- plus the trap where a catch (...) that does not rethrow aborts the process with FATAL: exception not rethrown. Verified on the Fedora 44 host with glibc 2.43: verify.lua PASS 45/FAIL 0, local, unprivileged, offline, with std::thread::native_handle_type static_asserted to be pthread_t." +duration: "55 minutes" +--- + +Chapter 49 measured threads from above. `std::thread` spawned them, +`sched_getcpu` reported where they landed, and the whole argument — that +concurrency is structure and parallelism is execution — went through without +ever asking what a thread *is* on this operating system. + +This chapter asks. On Linux, a `std::thread` is a pthread; on this toolchain +that is not an analogy but a type identity you can assert at compile time. And +a pthread can do a great deal that `std::thread` cannot express: it has a name +the kernel will show you, a stack whose size you choose, an affinity mask of +its own, a scheduling policy, and a cancellation mechanism the C++ committee +looked at and declined to standardize. Every one of those is reachable from +standard C++ through exactly one door, `native_handle()`, and every one of them +has an effect you can go and observe in `/proc`. + +{% include excalidraw.html + file="50-thread-two-identities" + alt="One thread drawn twice, in two bands. The upper band, the C++ view described as portable and deliberately narrow, holds a std::thread box listing an opaque std::thread::id, join/detach/joinable, hardware_concurrency, and no name, no stack, no affinity; an amber native_handle box calling it the sanctioned escape hatch and noting native_handle_type IS pthread_t here by static_assert at compile time; and a std::jthread plus stop_token box describing cooperative cancellation as a flag the target checks with no unwinding and no exception, the answer to pthread_cancel. The lower band, the Linux view of the same thread as the kernel and glibc see it, holds a pthread_t box calling it an opaque glibc handle to be compared with pthread_equal and explicitly not a number and not a tid; an amber gettid box calling it the kernel task id, a real integer that names a /proc/self/task// directory, noting that for the main thread tid equals pid; and three summary boxes for naming via pthread_setname_np into procfs comm with 15 characters maximum and 16 refused as ERANGE, stack via pthread_attr_setstacksize giving 262144 with anything below PTHREAD_STACK_MIN of 16384 refused as EINVAL, and affinity applying to one thread with scheduling reporting SCHED_OTHER priority 0 and SCHED_FIFO refused as EPERM. An amber box states every facet still reports digest 0x481984990deee5ff. An arrow runs from std::thread to native_handle to jthread across the top, and an amber arrow drops from native_handle down into the gettid box." + caption="Figure 50.1 — one thread, two identities: what the C++ standard library exposes, what Linux actually offers, and the single door between them" %} + +> **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, used for the parity gate I), `lua` (host; gated as `lua >= 5.4`, runs +> `verify.lua`), and `python3` (host; gated, runs +> `scripts/test-all-examples.py`). `ps` from procps-ng is used in one optional +> observation step below and is part of the Fedora base install rather than a +> gate. No VM, no root, no LGTM stack, no network: `examples/50-pthreads` is +> `mode: local` in `examples/manifest.yaml` and builds from the standard +> library and pthreads alone. + +Like Chapter 49, this example ships `langs: [cpp]`. Go and Rust both sit on +these same pthreads and both deliberately hide exactly these controls — that is +the *point* of their threading models — so a three-language version would have +two directories with nothing to say. Every code block below is a plain fenced +`cpp`, `lua`, or `console` block. + +## Two identities, and neither is the other + +Start with the thing that trips people up in production logs. + +A Linux thread has two names. `pthread_t` is an opaque handle that glibc hands +you. The standard says you may compare it with `pthread_equal` and nothing +else — it is not a number, it is not guaranteed to be printable, and on some +platforms it is a pointer. `gettid()` is different in kind: it returns the +kernel's task id, a genuine integer, and it is the id that names a directory in +`/proc`, that `perf` records, that `ftrace` emits, that gdb's `info threads` +shows in its LWP column, and that `top -H` sorts by. + +If you log `pthread_self()` and then try to find that thread in `perf`, you +will not find it. The two identities do not convert. + +``` +[host]$ ./demo.sh cpp run identity +identity: main pid=2675825 tid=2675825 same=yes +identity: worker 0 tid=2675826 +identity: worker 1 tid=2675827 +identity: worker 2 tid=2675828 +identity: worker_tids_distinct=yes main_task_dir=present +pthreads report: facet=identity digest=0x481984990deee5ff +``` + +`main pid=2675825 tid=2675825 same=yes` is the line worth stopping on. The main +thread's task id *equals* the process id, and that is not a coincidence or an +implementation detail — on Linux a process **is** its first thread. Everything +else the kernel calls a thread is a task in the same thread group, sharing that +leader's pid as its tgid. Chapter 11 built the process side of this picture; +this is the same structure viewed from the other end. + +The consequence is a small family of bugs: code that calls `getpid()` inside a +worker thread expecting "which thread am I" gets the *process* id and is +therefore identical in every thread. `gettid()` is the answer, and since glibc +2.30 it is a plain libc function rather than a `syscall(SYS_gettid)` incantation. + +Each of those tids also names a real directory: + +```cpp +bool task_dir_exists(pid_t tid) { + return access(("/proc/self/task/" + std::to_string(tid)).c_str(), F_OK) == 0; +} +``` + +`/proc/self/task/` is the per-thread mirror of `/proc/self/`. Chapter 36 read +`/proc` by hand for process-wide facts; this subtree is where the per-thread +ones live, and the next facet writes to it. + +## Naming: the kernel will remember what you call a thread + +`std::thread` has no name. Not a limited one, not an awkward one — none at all. +That is a real operational cost: a core dump, a `top -H`, or a gdb backtrace +from a program with twenty `std::thread`s shows twenty identical rows. + +`pthread_setname_np` fixes it, and the fix is not glibc bookkeeping — the name +lands in the kernel's `task_struct` and becomes readable from procfs: + +```cpp +std::string comm_of(pid_t tid) { + std::ifstream f("/proc/self/task/" + std::to_string(tid) + "/comm"); + std::string s; + std::getline(f, s); + return s; +} +``` + +The example sets a name and then reads it back through *both* available paths — +glibc's own getter and the kernel's file — to show they are the same string: + +``` +[host]$ ./demo.sh cpp run naming +naming: tid=2675834 via_pthread='ch50-worker' via_proc='ch50-worker' agree=yes +naming: 16char_rc=34 (ERANGE) name_after='ch50-worker' unchanged=yes +pthreads report: facet=naming digest=0x481984990deee5ff +``` + +The second line is the part worth knowing before you need it. The kernel's +`comm` field is **16 bytes including the terminating NUL**, so fifteen +characters is the most that fits. glibc does not truncate an over-long name; it +refuses the whole call with `ERANGE` and leaves the previous name in place: + +```cpp + // 16 characters: one too many for a 16-byte field that must hold a NUL. + too_long_rc = pthread_setname_np(pthread_self(), "0123456789abcdef"); + after_too_long = comm_of(tid); +``` + +If you do not check the return value — and almost nobody checks the return +value of a naming call — an over-long name is a silent no-op. Your thread keeps +whatever it was called before, which is the process name, which is the +uninformative default you were trying to escape. The gate asserts both halves: +that the call returned `ERANGE`, and that the old name survived. + +Which layer refuses matters here. glibc is what returns `ERANGE`; the kernel's +`prctl(PR_SET_NAME)` interface silently truncates instead. Take a different +route to the same field and you get a different failure mode for the same +mistake. + +## Stack size: the control `std::thread` never offered + +`std::thread` gives you no say in stack size. You get the default, and on glibc +the default is not one number but two: + +``` +[host]$ ./demo.sh cpp run stack +stack: main=8376320 default_worker=8388608 custom=262144 requested=262144 set_rc=0 +stack: custom_smaller_than_default=yes custom_at_least_requested=yes +stack: PTHREAD_STACK_MIN=16384 below_min_rc=22 (EINVAL) +pthreads report: facet=stack digest=0x481984990deee5ff +``` + +The main thread reports 8376320 bytes and a created thread reports 8388608. +Those differ because they come from different places: the main thread's stack +is grown on demand by the kernel up to `RLIMIT_STACK`, so what +`pthread_getattr_np` reports is derived from that rlimit and is not a round +number. A created thread's stack is a plain `mmap` of exactly 8 MiB. Two +thread-shaped things in one process with two different stack stories. + +Why care? Because 8 MiB is *virtual* address space per thread, and a program +that spawns a few thousand threads is asking for tens of gigabytes of mappings. +It usually works — the pages are untouched and therefore never backed — but it +interacts badly with `vm.overcommit_memory`, with cgroup memory accounting, and +with 32-bit address spaces. Anyone who has built a thread-per-connection server +has met this. + +```cpp + std::size_t custom_size = 0; + pthread_attr_t attr; + pthread_attr_init(&attr); + const int set_rc = pthread_attr_setstacksize(&attr, kRequested); + pthread_t c{}; + pthread_create(&c, &attr, stack_probe, &custom_size); + pthread_join(c, nullptr); + pthread_attr_destroy(&attr); +``` + +256 KiB requested, exactly 262144 observed. The gate asserts `>=` rather than +`==` deliberately: a page-rounded value is permitted by the standard and would +be just as correct, so asserting strict equality would be gating on an +implementation detail this chapter does not actually care about. + +Going too small is refused rather than rounded up: + +```cpp + pthread_attr_t small; + pthread_attr_init(&small); + const int too_small_rc = pthread_attr_setstacksize(&small, 8192); + pthread_attr_destroy(&small); +``` + +`EINVAL`, because 8192 is below this platform's `PTHREAD_STACK_MIN` of 16384. +That floor is not arbitrary — it has to hold the thread descriptor, TLS, and +enough room for a signal frame. Note that `PTHREAD_STACK_MIN` is not a compile +time constant on glibc; it expands to a `sysconf` call, which is why it is +printed at run time here rather than baked in. + +## Affinity: per thread, not per process + +Chapter 49 pinned with `sched_setaffinity(0, ...)`, and that zero means *this +whole process*. It was the right call there — the goal was to remove +parallelism from the entire program to isolate the concurrency axis. But it is +a blunt instrument, and it is not what you want when a low-latency fast path +should own a core while background work stays out of its way. + +`pthread_setaffinity_np` moves one thread: + +```cpp +void* pin_probe(void* raw) { + auto* a = static_cast(raw); + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(static_cast(a->cpu), &set); + a->rc = pthread_setaffinity_np(pthread_self(), sizeof set, &set); + // sched_yield gives the kernel an obvious moment to honour the new mask; + // it is not required for correctness, only for a prompt observation. + sched_yield(); + a->observed = sched_getcpu(); + return nullptr; +} +``` + +``` +[host]$ ./demo.sh cpp run affinity +affinity: worker_a asked=0 rc=0 observed_cpu=0 +affinity: worker_b asked=1 rc=0 observed_cpu=1 +affinity: workers_on_different_cpus=yes main_cpus_allowed=16 unchanged=yes +pthreads report: facet=affinity digest=0x481984990deee5ff +``` + +Two threads, two CPUs, and `main_cpus_allowed=16 unchanged=yes` — the calling +thread's mask was never touched. Run Chapter 49's `concurrent` model and the +process-wide call would have left `cpus_allowed=1` for everything. + +This is the mechanism behind Chapter 40's pinned fast path, and it is worth +knowing that the C++ standard has no spelling for it at all. There is no +`std::thread::set_affinity`, no executor property in C++23 that reaches it. +`native_handle()` is the whole story. + +The gate skips rather than fails on a host allowed fewer than two CPUs, because +two threads cannot occupy two CPUs that do not exist: + +```lua +if aff:find("need at least 2 to pin two threads apart") then + print("SKIP: fewer than 2 CPUs allowed -- gate E not asserted") +else +``` + +An informational `SKIP` beside the PASS count, never a silent pass — the same +discipline Chapter 48 used for absent tools. + +## Scheduling: the facet whose interesting call fails + +Scheduling policy is per-thread too, and `std::thread` cannot touch it either. + +``` +[host]$ ./demo.sh cpp run sched +sched: get_rc=0 policy=0 is_other=yes priority=0 +sched: SCHED_FIFO prio 50 -> rc=1 (EPERM) policy_after=0 unchanged=yes +pthreads report: facet=sched digest=0x481984990deee5ff +``` + +The default is `SCHED_OTHER` at priority 0. That zero is required, not +incidental: under the fair scheduler the `sched_param` priority *must* be zero, +and the knob that actually influences `SCHED_OTHER` scheduling is nice, which +lives behind a different call entirely. A `sched_param` with a nonzero priority +and `SCHED_OTHER` is rejected. Two things both called priority, only one of them +in this struct. + +Then the interesting call, which fails: + +```cpp + sched_param rt{}; + rt.sched_priority = 50; + const int fifo_rc = pthread_setschedparam(pthread_self(), SCHED_FIFO, &rt); +``` + +`EPERM`. Real-time policy needs `CAP_SYS_NICE` or an `RLIMIT_RTPRIO` allowance, +and an unprivileged process has neither. + +The refusal is the observable, and that is a deliberate design decision for this +example rather than a limitation of it. A demo that needed root to show anything +would not run on a reader's laptop, would not run in the runner, and would have +to be gated behind the VM lab. What an unprivileged process *can* prove is that +the policy is what it says, that the request was refused with the specific errno +the manual documents, and that the refusal was clean — `unchanged=yes`, the +thread is still on `SCHED_OTHER`. The gate asserts exactly that, and `verify.lua` +prints a line saying which stronger claim it is not making: + +``` +info: a SUCCESSFUL SCHED_FIFO change is not gated -- it needs CAP_SYS_NICE or an RLIMIT_RTPRIO allowance; the EPERM refusal above is what an unprivileged run can prove +``` + +## Cancellation, and why C++ said no + +Here is the facility C++ looked at and declined. + +{% include excalidraw.html + file="50-cancellation-two-roads" + alt="Two bands comparing cancellation mechanisms. The amber upper band, titled pthread_cancel as asynchronous and it unwinds, runs left to right: a worker parked box noting cancellation points like sleep and read; an amber pthread_cancel box saying glibc throws a forced unwind through the target's stack; a RAII really runs box listing that the Guard destructor ran, the cleanup handler ran, and join returned PTHREAD_CANCELED; and a side box observing this is better than its reputation because destructors are not skipped. Below them a dashed ghost box reads: but catch parenthesis dot dot dot without a rethrow swallows it, FATAL colon exception not rethrown leading to SIGABRT exit 134, and ordinary defensive C++ turns a cancel into a dead process. The lower band, titled std::jthread plus stop_token as cooperative and C++20's answer, runs left to right: request_stop sets a flag with no exception and no unwind; stop_token::stop_requested is checked by the target when it chooses to; and an amber box concluding the target decides where it is safe to stop, with no cancellation points, no forced unwind, and nothing a catch-all can accidentally swallow." + caption="Figure 50.2 — two roads out of a running thread: pthread_cancel does unwind and does run destructors, but the mechanism is an exception a catch-all can swallow fatally; stop_token is the cooperative alternative C++20 chose" %} + +`pthread_cancel` has a reputation for being unsafe in a way that turns out to be +half wrong, and the half that is wrong matters: + +``` +[host]$ ./demo.sh cpp run cancel +cancel: worker parked on a cancellation point +cancel: ~Guard(raii) ran during unwinding +cancel: pthread_cleanup handler ran (holding a-resource) +cancel: pthread_cancel rc=0 joined_retval_is_canceled=yes +pthreads report: facet=cancel digest=0x481984990deee5ff +``` + +Cancellation on glibc **unwinds the stack**. The C++ destructor ran. The +`pthread_cleanup_push` handler ran. `pthread_join` reported `PTHREAD_CANCELED` +rather than a normal return value. RAII is not silently skipped, which is more +than most descriptions of `pthread_cancel` will tell you. + +Note the order in the output: `~Guard` ran *before* the cleanup handler, because +the handler was pushed first and cleanup handlers pop last-in-first-out just +like destructors. The two mechanisms interleave correctly rather than one +running to the exclusion of the other. + +So what is the objection? The mechanism. That unwind is implemented as a special +exception, and glibc requires it to reach the top of the thread. Ordinary, +defensive, reviewed-and-approved C++ can stop it: + +```cpp +void* swallow_worker(void*) { + try { + std::printf("cancel: worker parked inside catch (...)\n"); + std::fflush(stdout); + for (;;) { + sleep(1); + } + } catch (...) { + // Looks like ordinary defensive C++. It is fatal here: the forced + // unwind must be allowed to propagate, and glibc aborts if it is not. + std::printf("cancel: caught the forced unwind and did not rethrow\n"); + std::fflush(stdout); + } + return nullptr; +} +``` + +``` +[host]$ ./demo.sh cpp run cancel-swallow +cancel: worker parked inside catch (...) +cancel: caught the forced unwind and did not rethrow +FATAL: exception not rethrown +Aborted (core dumped) +``` + +Exit 134. Not the thread — the *process*. A `catch (...)` with no rethrow is a +pattern that appears in every large C++ codebase, usually at a thread entry +point, usually written precisely to stop one thread's failure from killing the +program. Combine it with `pthread_cancel` and it does the exact opposite of what +it was written to do. + +That is the argument C++20 answered with `std::jthread` and `std::stop_token`. +Cooperative cancellation is a flag: `request_stop()` sets it, the target polls +`stop_requested()` wherever it decides is safe, and there is no unwinding, no +cancellation points to enumerate, and nothing for a catch-all to swallow. It +does less than `pthread_cancel` — a thread blocked in a syscall that never +checks the token will not stop — and that reduction is the whole design. + +Both cancellation behaviors are hard-gated here, including the abort: + +```lua +local swallow = checks.run("./demo.sh cpp run cancel-swallow 2>&1") +checks.expect_match(tostring(swallow.exit ~= 0), "true", + "cpp: swallowing the forced unwind kills the process (exit=" .. tostring(swallow.exit) .. ")") +checks.expect_match(swallow.out, "exception not rethrown", + "cpp: glibc names the reason -- a catch (...) that does not rethrow is fatal here") +``` + +The match is deliberately loose — `exception not rethrown` rather than the full +`FATAL: exception not rethrown` — because the surrounding text is a glibc +string and this gate is about the behavior, not about glibc's punctuation. + +## The bridge: `std::thread` *is* a pthread here + +Everything above used raw `pthread_create`, but none of it had to. The two +worlds compose, and the compiler will confirm it: + +```cpp +static_assert(std::is_same_v, + "on this platform std::thread::native_handle() is a pthread_t"); +``` + +That is not a runtime check that happened to pass. It is a compile-time +assertion that the standard type and the POSIX type are the same type on this +platform. So you can create a thread the standard way and control it the POSIX +way: + +``` +[host]$ ./demo.sh cpp run bridge +bridge: native_handle_is_pthread_t=yes setname_rc=0 tid=2675865 proc_comm='std-thread-x' +pthreads report: facet=bridge digest=0x481984990deee5ff +``` + +A `std::thread`, named from the outside through its native handle, and the name +is visible in that thread's procfs entry. + +Using `native_handle()` is not a hack, and it is worth saying so plainly because +it often gets treated as one in review. It is in the standard *specifically* so +that platform facilities the standard does not wrap remain reachable. The +standard's own position is that it will not try to portably abstract thread +naming, stack sizing, affinity, or scheduling — and that you should still be +able to do all four. + +The caveats are real but narrow. `native_handle_type` is implementation-defined, +so the `static_assert` above is doing load-bearing work rather than decoration — +on Windows the same call yields a `HANDLE` and none of this chapter compiles. +And the handle is only valid while the `std::thread` is joinable; using it after +`join()` is a use-after-free with a POSIX-shaped name. + +## How the code works + +Each facet is its own subcommand, and that structure is load-bearing rather +than tidy: + +```cpp + if (facet == "identity") { + facet_identity(); + } else if (facet == "naming") { + facet_naming(); + } else if (facet == "stack") { + facet_stack(); +``` + +`cancel-swallow` ends in `abort()` by design. If the facets shared a process, +that one would take the other seven with it and the harness would be asserting +on a corpse. Chapter 49 split its contract semantics into four binaries for the +same reason; this chapter splits one binary into subcommands, which is cheaper +and sufficient because only one facet is fatal. + +The digest is carried forward unchanged: + +```cpp +void report(const char* facet) { + std::printf("pthreads report: facet=%s digest=0x%016llx\n", facet, + static_cast(fnv1a(kPayload, sizeof kPayload))); +} +``` + +Every facet prints it, and gate A checks all seven. The claim is the same one +Chapter 49 made about execution models, narrowed one level: turning a per-thread +knob — renaming a thread, shrinking its stack, pinning it, cancelling it — +changes where and how the work runs and not what it computes. + +## Errors, three ways + +The three failure kinds in this program get three different treatments, and the +choice is the interesting part. + +**A refusal that is the point.** `pthread_setschedparam` returning `EPERM` is +not an error path to be handled and hidden — it is the facet's result. It gets +printed with its errno name and asserted on. Failing is what an unprivileged +process is *supposed* to do here, and a demo that silently skipped the call +would have taught nothing. + +**A refusal that is a trap.** `pthread_setname_np` returning `ERANGE` is a real +error that almost every caller ignores, because naming a thread feels +consequence-free. The example checks it and shows what ignoring it costs — the +thread silently keeps its old name: + +```cpp + std::printf("naming: 16char_rc=%d (%s) name_after='%s' unchanged=%s\n", too_long_rc, + too_long_rc == ERANGE ? "ERANGE" : std::strerror(too_long_rc), after_too_long.c_str(), + after_too_long == via_proc ? "yes" : "no"); +``` + +**A failure that cannot be handled at all.** Swallowing the forced unwind does +not produce an errno, an exception you can catch, or a return code. glibc +prints one line and aborts. There is no error handling for this one — only +avoidance, which is precisely why the language grew a different mechanism. + +The pattern: report the refusals that are informative, check the refusals that +are silent, and recognize the failures whose only handling is a different design. + +## Concurrency lens + +This example is about threads, so the lens turns on the example itself. + +The facets synchronize almost entirely through `pthread_join` and +`std::thread::join`, and that is a deliberate simplification: `join` is a full +synchronization edge, so a worker can write to a plain `std::string` or +`std::vector` slot and the joining thread can read it afterwards with no atomics +and no mutex. `facet_naming` does exactly that with three ordinary variables +captured by reference. The pattern is safe because of the join, not despite it — +remove the join and every one of those reads becomes a data race. + +One facet does need an atomic, and it is worth seeing why: + +```cpp + std::thread t([&] { + tid = gettid(); + while (!named.load(std::memory_order_acquire)) { + sched_yield(); + } + name = comm_of(gettid()); + }); + + // Name a std::thread from the outside, through its native handle. + const int rc = pthread_setname_np(t.native_handle(), "std-thread-x"); + named.store(true, std::memory_order_release); +``` + +The bridge facet has a genuine ordering requirement that no `join` can supply: +the worker must not read its own `comm` until the *main* thread has finished +naming it. That is a happens-before edge in the middle of both threads' lives, +which is what release/acquire is for — the release store publishes the naming, +the acquire load consumes it. Chapter 26 built this machinery; here is the +smallest real use of it in the book. + +The spin uses `sched_yield()` rather than a busy loop or a sleep. A raw spin +would burn a CPU for no reason on a possibly single-CPU host, and a sleep would +put a duration into a chapter that gates on none. `sched_yield` says "someone +else should run now," which is exactly the intent. + +The affinity facet's `sched_yield()` is a different thing wearing the same name. +There it is not synchronization at all — the affinity call has already +succeeded and the mask is already installed. It only gives the kernel an +obvious moment to act on the new mask before `sched_getcpu()` samples, so the +observation is prompt. The comment in the source says so, because a `yield` +with no explanation reads like a synchronization bug to the next person. + +## Build, run, observe + +Local, unprivileged, offline. + +``` +[host]$ cd examples/50-pthreads +[host]$ ./demo.sh cpp build +[host]$ for f in identity naming stack affinity sched cancel bridge; do ./demo.sh cpp run "$f"; done +[host]$ ./demo.sh cpp run cancel-swallow # aborts on purpose, exit 134 +``` + +The facets are short, but `cancel` parks its worker for about 200 ms, which is +long enough to catch from another terminal — and seeing the two identities in a +tool other than our own program is worth the extra step: + +``` +[host]$ ./cpp/build/release/pthreads cancel & sleep 0.1 +[host]$ ps -L -o pid,tid,comm,psr -p "$(pgrep -x pthreads)" + PID TID COMMAND PSR +2676217 2676217 pthreads 2 +2676217 2676219 pthreads 14 +``` + +Two rows, one process. The first row's `TID` equals the `PID` — the same fact +the `identity` facet reported, now confirmed by a tool that has never heard of +this program. `PSR` is the CPU each thread is on, which the `affinity` facet is +what lets you choose. `COMMAND` is the procfs `comm` field, showing the process +name here because this particular facet does not rename its worker. + +Then the harness: + +``` +[host]$ LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua +``` + +``` +ok: cpp: a 16-character name is refused with ERANGE (comm is 16 bytes incl. NUL) +ok: cpp: the refused rename left the previous name in place -- a silent no-op if unchecked +ok: cpp: the thread created with a 256 KiB attr got at least 256 KiB (custom=262144) +ok: cpp: and materially less than the default (default_worker=8388608) +ok: cpp: a stack below PTHREAD_STACK_MIN is refused with EINVAL, not rounded up +ok: cpp: two threads pinned with pthread_setaffinity_np ran on two different CPUs +ok: cpp: pinning those threads left the caller's own affinity mask alone (main_cpus_allowed=16) -- unlike ch49's process-wide pin +ok: cpp: an unprivileged SCHED_FIFO request is refused with EPERM +ok: cpp: pthread_cancel unwinds the stack -- the C++ destructor really ran +ok: cpp: swallowing the forced unwind kills the process (exit=134) +ok: cpp: glibc names the reason -- a catch (...) that does not rethrow is fatal here +ok: cpp: std::thread::native_handle_type is pthread_t (static_assert, compile time) +ok: cpp: and procfs shows the name on the std::thread's own task +PASS 45 / FAIL 0 +``` + +## What the standard still does not give you + +Chapter 49 had two live C++26 features to hard-gate. This chapter has none, and +that absence is itself the finding rather than a gap to paper over: pthreads is +POSIX, and the C++ standard's position on all six controls above has not moved. + +As of C++23, and on this toolchain with GCC 16 and clang 22, the standard +library offers no thread naming, no stack-size control, no affinity, and no +scheduling policy. `std::thread` gives you construction, `join`, `detach`, +`joinable`, an opaque `id`, and `hardware_concurrency()`. C++20 added +`std::jthread` and `std::stop_token` — which is a real addition, and this +chapter's cancellation section is the argument for why it was the right one — +but it does not widen the surface toward any of the other five. + +That is a defensible position rather than an oversight. Thread naming has +different length limits on every platform, affinity does not exist in the same +shape on all of them, and a portable `set_stack_size` would have to say +something about platforms with no such concept. The committee's answer is +`native_handle()`: refuse to abstract what cannot be abstracted cleanly, and +guarantee an escape hatch instead. + +The practical consequence for the rest of Part 14 is worth stating now. Every +higher-level model in this compendium — Boost.Thread, coroutines, fibers, +Asio — sits on top of this same layer. When one of them exposes a control the +standard does not, this is where it is coming from, and when one of them cannot +expose something, this is the layer you drop to. + +## Cross-check: same digest, one level down + +Every facet reported `digest=0x481984990deee5ff`, and clang agrees: + +``` +[host]$ cd cpp && cmake --preset release-clang && cmake --build --preset release-clang --target pthreads +[host]$ ./build/release-clang/pthreads identity +``` + +That constant now spans Chapter 46's C++ toolbox, Chapter 47's Go, Chapter 48's +Rust, Chapter 49's four execution models, and Chapter 50's six per-thread +controls under two compilers. Three languages, four execution models, six +knobs, one number. + +The claim narrows appropriately each time. Chapter 49 said the execution model +does not change the answer. This chapter says the same about the per-thread +configuration: renaming a thread, shrinking its stack to 256 KiB, pinning it to +CPU 0, or cancelling it mid-flight changes where and how the work happens, and +not what it computes. + +## What you learned + +- **A thread has two identities.** `pthread_t` is an opaque glibc handle you + may only compare; `gettid()` is the kernel's task id that `/proc`, `perf`, + `ps -L`, and gdb all speak. They do not convert. +- **A process is its first thread.** The main thread's tid equals the pid, + which is why `getpid()` inside a worker answers a different question than the + one you meant to ask. +- **`/proc/self/task//` is the per-thread mirror of `/proc/self/`.** + Thread names written with `pthread_setname_np` show up there, and in every + tool that reads it. +- **`comm` is 16 bytes including the NUL.** Fifteen characters maximum; glibc + refuses a longer name with `ERANGE` and leaves the old one in place, so an + unchecked naming call is a silent no-op. +- **Stack size is a real per-thread decision.** 8 MiB by default per created + thread, tunable with `pthread_attr_setstacksize`, floored at + `PTHREAD_STACK_MIN` with `EINVAL` rather than silent rounding. +- **Affinity and scheduling policy are per-thread.** Chapter 49's + `sched_setaffinity(0, ...)` moved the whole process; + `pthread_setaffinity_np` moves one thread and leaves the caller alone. +- **A refusal can be the observable.** An unprivileged `SCHED_FIFO` request + fails with `EPERM`, and asserting that specific clean refusal is a better + gate than a demo that would need root to show anything. +- **`pthread_cancel` does unwind — that is not the problem.** Destructors and + cleanup handlers both run and `join` reports `PTHREAD_CANCELED`. The problem + is that the mechanism is an exception, and an ordinary `catch (...)` without + a rethrow aborts the whole process with `exception not rethrown`. +- **That is why `std::stop_token` exists.** Cooperative cancellation does + strictly less, on purpose: a flag the target checks where it chooses, with + nothing for a catch-all to swallow. +- **`native_handle()` is sanctioned, not a hack.** It is in the standard + precisely so the six controls above stay reachable — and on this platform + `native_handle_type` is `pthread_t`, asserted at compile time. + +--- + +

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, 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 45 / FAIL 0 with gate E (affinity) and gate I (clang +parity) both running for real rather than skipping, and python3 +scripts/test-all-examples.py --only 50-pthreads reported 1 passed, +0 failed, 0 skipped. Every transcript quoted above is a real run from +this session. Confirmed live: all seven non-aborting facets produced +digest=0x481984990deee5ff, byte-identical to Chapter 46's C++, +Chapter 47's Go, Chapter 48's Rust, and Chapter 49's conc; +identity reported main pid=2675825 tid=2675825 same=yes +with three distinct worker tids; naming reported the same +'ch50-worker' through both pthread_getname_np and +/proc/self/task/<tid>/comm, with a 16-character name refused +rc=34 (ERANGE) and the previous name intact; stack +reported main=8376320 default_worker=8388608 custom=262144 and +below_min_rc=22 (EINVAL) against +PTHREAD_STACK_MIN=16384; affinity put two workers on +CPUs 0 and 1 with main_cpus_allowed=16 unchanged=yes; +sched reported policy=0 (SCHED_OTHER) at +priority 0 with SCHED_FIFO refused rc=1 (EPERM) and +the policy unchanged; cancel ran ~Guard(raii) and the +pthread_cleanup_push handler and reported +joined_retval_is_canceled=yes; cancel-swallow printed +FATAL: exception not rethrown and died of SIGABRT with exit 134; +and bridge named a std::thread through +native_handle() with proc_comm='std-thread-x', the +static_assert on native_handle_type having already +passed at compile time. The ps -L transcript is a real capture of a +live cancel run. Not exercised: unverified — a successful real-time policy +change is not gated anywhere in this example. It requires +CAP_SYS_NICE or an RLIMIT_RTPRIO allowance that this +unprivileged run does not have, so the chapter's claim is limited to the clean +EPERM refusal that was measured, and verify.lua prints +an informational line naming the stronger claim it is declining to make.

diff --git a/_plans/r19-ch50-plan.md b/_plans/r19-ch50-plan.md new file mode 100644 index 0000000..edc0aa3 --- /dev/null +++ b/_plans/r19-ch50-plan.md @@ -0,0 +1,164 @@ +--- +title: "r19 / ch50 — pthreads — plan (internal)" +published: false +--- + +# r19 ch50 — example `50-pthreads` + chapter + diagrams + +Second chapter of Part 14 (Compendium: C++ Concurrency). ch49 set the vocabulary +(concurrency is structure, parallelism is execution) and measured it with `std::thread`. +ch50 goes **under** `std::thread` to the thing it is on Linux: a pthread. + +The thesis: `std::thread` is a portable wrapper that deliberately exposes none of the +per-thread controls Linux actually offers. Every one of those controls is reachable +through `native_handle()`, and every one of them has an effect visible in `/proc` or in +a scheduler syscall. This is the chapter where a thread stops being an abstraction and +starts being a task with a tid, a name, a stack, an affinity mask, and a policy. + +## Decisions (answered 2026-08-16) + +- **D1 — Six facets.** identity, naming, stack sizing, per-thread affinity, scheduling + policy, cancellation. Scheduling is included even though the interesting call + (`SCHED_FIFO`) *fails* unprivileged — the refusal is the observable. +- **D2 — Hard-gate both cancellation behaviors.** Destructors run during + `pthread_cancel`, AND swallowing the forced unwind aborts with + `FATAL: exception not rethrown`. +- **D3 — No forced C++26 section.** Instead "what the standard still does not give you", + grounded in measured facts, with `native_handle()` as the sanctioned bridge. +- **D4 — Autonomous through PR; the user merges.** + +## Host audit (run 2026-08-16, Fedora 44, kernel 7.1.5-201.fc44) + +All measured with real probe programs, not inferred: + +| fact | measured value | +| --- | --- | +| glibc | **2.43** | +| `PTHREAD_STACK_MIN` | **16384** | +| main thread: `gettid()` vs `getpid()` | **equal** (2483509 both) — the main thread's tid *is* the pid | +| main thread stack (`pthread_getattr_np`) | 8376320 (~7.99 MiB) — RLIMIT_STACK-derived, *not* a round number | +| worker default stack | **8388608** (exactly 8 MiB) — differs from main's | +| `pthread_setname_np`, 15 chars | 0 (ok) | +| `pthread_setname_np`, 16 chars | **ERANGE (34)** — kernel `comm` is 16 bytes *including* the NUL | +| name visible at | `/proc/self/task//comm` | +| `pthread_attr_setstacksize(256 KiB)` | 0; worker then observes **exactly 262144** | +| `pthread_attr_setstacksize(8 KiB)` | **EINVAL (22)** — below `PTHREAD_STACK_MIN` | +| `pthread_setaffinity_np(cpu 3)` | 0; worker `sched_getcpu()`=3 while **main stayed on cpu 1** | +| `pthread_getschedparam` | policy=0 (`SCHED_OTHER`), prio=0 | +| `pthread_setschedparam(SCHED_FIFO, 50)` unprivileged | **EPERM (1)** | +| `pthread_cancel` + `pthread_cleanup_push` | handler ran; `pthread_join` retval == `PTHREAD_CANCELED` | +| `pthread_cancel` vs C++ RAII | **destructors run** — glibc unwinds | +| `catch (...)` without rethrow during cancel | **`FATAL: exception not rethrown`, exit 134** | +| `std::thread::native_handle_type` | `static_assert(is_same_v<..., pthread_t>)` **passes** | +| `__cpp_lib_jthread` | 201911 | + +Toolchain unchanged from r18: GCC 16.1.1, clang 22.1.8, CMake 4.3.0, Ninja 1.13.0, +16 logical / 8 physical CPUs. Local, unprivileged, offline. No Boost yet (ch52 starts that). + +## The core demo — `pthreads` (C++-only, `langs: [cpp]`) + +One program, one subcommand per facet, so a trapping or aborting facet can never take +the others with it. Digest continuity: every facet still reports +`digest=0x481984990deee5ff` over `"The quick brown."` (ch46/47/48/49's payload). + +Facets and their observables: + +1. **identity** — `pthread_t` (opaque handle) vs `gettid()` (the kernel's task id) vs + `getpid()`. Assert main's tid == pid, workers' tids differ from both and from each + other, and each tid has a directory under `/proc/self/task/`. +2. **naming** — `pthread_setname_np`, read back two ways: `pthread_getname_np` and + `/proc/self/task//comm`. Assert both agree, and that a 16-char name is refused + with `ERANGE` while the previous name survives unchanged. +3. **stack** — one thread at the default, one at 256 KiB via + `pthread_attr_setstacksize`. Assert the custom thread observes exactly 262144 via + `pthread_getattr_np`, that it differs from the default, and that a request below + `PTHREAD_STACK_MIN` is refused with `EINVAL`. +4. **affinity** — `pthread_setaffinity_np` per thread. Assert worker A and worker B + report *different* `sched_getcpu()` values and that the main thread's mask is + unchanged — the ch49 contrast: that chapter pinned the whole process, this one pins + one thread. +5. **sched** — `pthread_getschedparam` reports `SCHED_OTHER`/0; an unprivileged + `SCHED_FIFO` request is refused with `EPERM`. Gate the refusal, not a success. +6. **cancel** — two sub-cases, both hard-gated per D2. + +## Gate tiers + +**Hard (A–G), all local, unprivileged, offline:** +- A. digest identical across all six facets + `consensus=yes`. +- B. identity: main tid == pid; worker tids distinct; `/proc/self/task/` exists. +- C. naming: `pthread_getname_np` and `/proc` `comm` agree; 16 chars → `ERANGE` and the + old name is intact. +- D. stack: custom == 262144, default != custom, sub-minimum → `EINVAL`. +- E. affinity: two workers report different CPUs; main's `cpus_allowed` unchanged. +- F. sched: policy is `SCHED_OTHER`; `SCHED_FIFO` → `EPERM`. +- G. cancel: cleanup handler ran AND `~Guard` ran AND retval == `PTHREAD_CANCELED`; + the swallow case aborts with `FATAL: exception not rethrown`. + +**Gated-if-present (H):** clang parity on the digest, as ch49. + +**Not gated:** anything needing root (`SCHED_FIFO` success, `RLIMIT_RTPRIO`). Named in +the chapter as requiring privilege, with the `EPERM` we *do* get as the evidence. + +## Risks + +- **Affinity gate E on a 1-CPU host.** Two threads cannot occupy two CPUs if only one + exists. Guard: read `CPU_COUNT(sched_getaffinity)` first and print an informational + SKIP for E when it is 1, exactly as ch48 handled absent tools. Never a false PASS. +- **`FATAL: exception not rethrown` is a glibc string.** It is glibc 2.43 here and the + message has been stable for many releases, but the gate should match loosely + (`exception not rethrown`) and the footer must name the glibc version. +- **Stack size rounding.** 256 KiB came back exactly, but a page-rounded value is + permitted. Assert `>= 262144` and `< default`, not strict equality, and say so. +- **Thread names are truncated, not rejected, by the kernel** when set through other + paths. Our path is `pthread_setname_np`, which returns `ERANGE` instead — assert the + glibc behavior we measured, and say which layer refuses. + +## Steps + +- S1 mint `examples/50-pthreads` via `scripts/new-example.sh`, strip go/rust, single-lang +- S2 write `cpp/src/pthreads.cpp` (six facets) + CMakeLists + demo.sh +- S3 write `verify.lua` (gates A–G hard, H skip-if-present, E skip-if-1-CPU) +- S4 register in `examples/manifest.yaml` (`langs: [cpp]`, `mode: local`) +- S5 capture all transcripts from real runs +- S6 write `_docs/50-pthreads.md` +- S7 2 diagrams + catalogue rows in `assets/diagrams/README.md` +- S8 gate matrix: verify.lua, runner, validate.py, verbatim-block check, banned words +- S9 PR (per D4, stop there) + +## Acceptance + +`LSP_LANG=cpp lua verify.lua` PASS/FAIL 0 with E and H running for real on this host; +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 and the exact gate record. + +## Outcome (2026-08-17) + +All steps complete. `LSP_LANG=cpp lua verify.lua` → **PASS 45 / FAIL 0**, with gate E +(affinity, needs ≥2 CPUs) and gate I (clang parity) both running for real on this host. +Runner → 1 passed. `validate.py` → OK. Banned-words clean. **15/15 chapter code blocks +verified as verbatim substrings** of `cpp/src/pthreads.cpp` or `verify.lua`; every +`Chapter N` cross-reference resolves to a real file. + +Deltas from the plan, all in the safe direction: + +- **Gate count 45, not the ~35 estimated** — the six facets each yielded more assertable + fields than planned once written. +- **Gate D asserts `>=` requested, not `==`.** Measured exactly 262144, but page rounding + is permitted and asserting equality would gate on an implementation detail. +- **The cancel-swallow gate matches `exception not rethrown` loosely**, without the + `FATAL:` prefix, since the surrounding text is a glibc string. glibc 2.43 named in the + footer as planned. +- **`~Guard` runs BEFORE the `pthread_cleanup_push` handler** — the handler was pushed + first and cleanup handlers pop LIFO, same as destructors. Worth a sentence in the + chapter; it shows the two mechanisms interleave rather than one excluding the other. +- **The `ps -L` observation step needed correcting before it shipped.** The first draft + claimed `comm` shows the `pthread_setname_np` name; measured, the `cancel` facet does + not rename its worker, so both rows show the process name. Text now says which facets + change it, and the transcript is a real capture. +- **`PTHREAD_STACK_MIN` is a `sysconf` call on glibc, not a compile-time constant** — + printed at run time in the demo rather than baked in. + +No C++26 section, per D3. The "what the standard still does not give you" section states +the measured absence (no naming, stack, affinity, or scheduling API in C++23 on GCC 16 / +clang 22) and lands on `native_handle()` as the sanctioned bridge, `static_assert`ed to +be `pthread_t` here. diff --git a/assets/diagrams/50-cancellation-two-roads.excalidraw b/assets/diagrams/50-cancellation-two-roads.excalidraw new file mode 100644 index 0000000..f8e390a --- /dev/null +++ b/assets/diagrams/50-cancellation-two-roads.excalidraw @@ -0,0 +1,1655 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1366087648", + "type": "rectangle", + "x": 24, + "y": 54, + "width": 1112, + "height": 278, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fdf7ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 222210039, + "versionNonce": 1100564262, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1420541909", + "type": "text", + "x": 36, + "y": 60, + "width": 297.0, + "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": 914019467, + "versionNonce": 878435211, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pthread_cancel \u2014 asynchronous, and it unwinds", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pthread_cancel \u2014 asynchronous, and it unwinds", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r655780925", + "type": "rectangle", + "x": 24, + "y": 350, + "width": 1112, + "height": 180, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1437152835, + "versionNonce": 719965062, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t810433939", + "type": "text", + "x": 36, + "y": 356, + "width": 363.00000000000006, + "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": 1000182718, + "versionNonce": 645395466, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::jthread + stop_token \u2014 cooperative, C++20's answer", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::jthread + stop_token \u2014 cooperative, C++20's answer", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1415578182", + "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": 142928853, + "versionNonce": 26408254, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1957165039", + "type": "text", + "x": 410, + "y": 22, + "width": 254.10000000000002, + "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": 248854227, + "versionNonce": 1967551690, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "two roads out of a running thread", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "two roads out of a running thread", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r276798116", + "type": "rectangle", + "x": 56, + "y": 96, + "width": 250, + "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": 1131719484, + "versionNonce": 1828340984, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1362078363", + "type": "text", + "x": 66, + "y": 104, + "width": 100.10000000000001, + "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": 1257510461, + "versionNonce": 798369287, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "worker parked", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "worker parked", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1147550131", + "type": "text", + "x": 66, + "y": 124, + "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": 1610423404, + "versionNonce": 1329993742, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "on a cancellation point", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "on a cancellation point", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t144648341", + "type": "text", + "x": 66, + "y": 139, + "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": 812140093, + "versionNonce": 1553242466, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "sleep(3), read(2), \u2026", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "sleep(3), read(2), \u2026", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1089235365", + "type": "rectangle", + "x": 346, + "y": 96, + "width": 250, + "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": 782077153, + "versionNonce": 297478696, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1712098145", + "type": "text", + "x": 356, + "y": 104, + "width": 130.9, + "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": 683155740, + "versionNonce": 201823297, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pthread_cancel(t)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pthread_cancel(t)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1465067438", + "type": "text", + "x": 356, + "y": 124, + "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": 1666442193, + "versionNonce": 466289127, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "glibc throws a forced unwind", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "glibc throws a forced unwind", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1902101568", + "type": "text", + "x": 356, + "y": 139, + "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": 1205586475, + "versionNonce": 1891160349, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "through the target's stack", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "through the target's stack", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1696493015", + "type": "rectangle", + "x": 636, + "y": 96, + "width": 230, + "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": 1922061790, + "versionNonce": 295330143, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1439178081", + "type": "text", + "x": 646, + "y": 104, + "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": 123653840, + "versionNonce": 1404374869, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "RAII really runs", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "RAII really runs", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t459122318", + "type": "text", + "x": 646, + "y": 124, + "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": 1387767815, + "versionNonce": 209655529, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "~Guard() ran", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "~Guard() ran", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1549818179", + "type": "text", + "x": 646, + "y": 139, + "width": 114.95, + "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": 869252979, + "versionNonce": 1915339327, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cleanup handler ran", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cleanup handler ran", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1658870835", + "type": "text", + "x": 646, + "y": 154, + "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": 330187312, + "versionNonce": 135140405, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "join \u2192 PTHREAD_CANCELED", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "join \u2192 PTHREAD_CANCELED", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1735400857", + "type": "rectangle", + "x": 906, + "y": 96, + "width": 200, + "height": 110, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1973120370, + "versionNonce": 1209290453, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t806770459", + "type": "text", + "x": 916, + "y": 104, + "width": 84.7, + "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": 1263500333, + "versionNonce": 203717291, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "better than", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "better than", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t376224547", + "type": "text", + "x": 916, + "y": 124, + "width": 90.75000000000001, + "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": 429159834, + "versionNonce": 203801964, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "its reputation:", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "its reputation:", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t966452066", + "type": "text", + "x": 916, + "y": 139, + "width": 90.75000000000001, + "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": 1579002226, + "versionNonce": 586792387, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "destructors are", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "destructors are", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1974358365", + "type": "text", + "x": 916, + "y": 154, + "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": 103594219, + "versionNonce": 523835924, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "not skipped", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "not skipped", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r749918663", + "type": "rectangle", + "x": 346, + "y": 228, + "width": 520, + "height": 84, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 624095285, + "versionNonce": 1224072693, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1511972978", + "type": "text", + "x": 356, + "y": 236, + "width": 354.20000000000005, + "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": 1998635201, + "versionNonce": 1368333409, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\u2026but catch (...) without a rethrow swallows it", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\u2026but catch (...) without a rethrow swallows it", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1844066204", + "type": "text", + "x": 356, + "y": 256, + "width": 308.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": 1113037168, + "versionNonce": 556036060, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "FATAL: exception not rethrown \u2192 SIGABRT, exit 134", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "FATAL: exception not rethrown \u2192 SIGABRT, exit 134", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t455564314", + "type": "text", + "x": 356, + "y": 271, + "width": 344.85, + "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": 1014285170, + "versionNonce": 1598796314, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ordinary defensive C++ turns a cancel into a dead process", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ordinary defensive C++ turns a cancel into a dead process", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r560175158", + "type": "rectangle", + "x": 56, + "y": 390, + "width": 250, + "height": 104, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 44945205, + "versionNonce": 1474440398, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t984790652", + "type": "text", + "x": 66, + "y": 398, + "width": 107.80000000000001, + "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": 351053596, + "versionNonce": 771853954, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "request_stop()", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "request_stop()", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1301151333", + "type": "text", + "x": 66, + "y": 418, + "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": 907501606, + "versionNonce": 1800888349, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "sets a flag", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "sets a flag", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1890553040", + "type": "text", + "x": 66, + "y": 433, + "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": 1313904009, + "versionNonce": 757578384, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no exception, no unwind", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no exception, no unwind", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r13313085", + "type": "rectangle", + "x": 346, + "y": 390, + "width": 250, + "height": 104, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 316397825, + "versionNonce": 367578390, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1153184796", + "type": "text", + "x": 356, + "y": 398, + "width": 215.60000000000002, + "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": 1410573242, + "versionNonce": 1275949624, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stop_token::stop_requested()", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stop_token::stop_requested()", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t370145301", + "type": "text", + "x": 356, + "y": 418, + "width": 102.85000000000001, + "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": 890106233, + "versionNonce": 743927764, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the target checks", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the target checks", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1569063100", + "type": "text", + "x": 356, + "y": 433, + "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": 442309771, + "versionNonce": 1088982156, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "when IT chooses to", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "when IT chooses to", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1781185237", + "type": "rectangle", + "x": 636, + "y": 390, + "width": 470, + "height": 104, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1727016953, + "versionNonce": 384876847, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1749193976", + "type": "text", + "x": 646, + "y": 398, + "width": 331.1, + "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": 1167913776, + "versionNonce": 1926001067, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the target decides where it is safe to stop", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the target decides where it is safe to stop", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1224430079", + "type": "text", + "x": 646, + "y": 418, + "width": 248.05, + "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": 155006528, + "versionNonce": 1163177535, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no cancellation points, no forced unwind,", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no cancellation points, no forced unwind,", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1198559486", + "type": "text", + "x": 646, + "y": 433, + "width": 266.20000000000005, + "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": 22431067, + "versionNonce": 1517059029, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "nothing a catch-all can accidentally swallow", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "nothing a catch-all can accidentally swallow", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1002626878", + "type": "arrow", + "x": 306, + "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": 346615801, + "versionNonce": 1476458273, + "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": "a385734185", + "type": "arrow", + "x": 596, + "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": 5181300, + "versionNonce": 595919712, + "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": "a160273259", + "type": "arrow", + "x": 606, + "y": 206, + "width": 0, + "height": 22, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 438357743, + "versionNonce": 617957142, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 22 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a120061625", + "type": "arrow", + "x": 306, + "y": 442, + "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": 408328507, + "versionNonce": 356118995, + "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": "a1513072123", + "type": "arrow", + "x": 596, + "y": 442, + "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": 440890968, + "versionNonce": 1863112395, + "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": "t1460607114", + "type": "text", + "x": 40, + "y": 550, + "width": 901.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": 831701458, + "versionNonce": 1806273707, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Both rows are real runs on this host (glibc 2.43). The upper row is why pthread_cancel is powerful; the dashed box is why C++ did not standardize it.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Both rows are real runs on this host (glibc 2.43). The upper row is why pthread_cancel is powerful; the dashed box is why C++ did not standardize it.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1494993766", + "type": "text", + "x": 40, + "y": 574, + "width": 919.6, + "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": 1696487655, + "versionNonce": 1209491852, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "The trap is not that cancellation skips cleanup \u2014 it does not. The trap is that the mechanism is an exception the caller must be careful never to catch.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "The trap is not that cancellation skips cleanup \u2014 it does not. The trap is that the mechanism is an exception the caller must be careful never to catch.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t747073651", + "type": "text", + "x": 40, + "y": 598, + "width": 713.9000000000001, + "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": 1658024898, + "versionNonce": 1317263134, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Both cancel cases are hard-gated in verify.lua, including the abort: exit 134 with 'exception not rethrown' on stderr.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Both cancel cases are hard-gated in verify.lua, including the abort: exit 134 with 'exception not rethrown' on stderr.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/50-cancellation-two-roads.svg b/assets/diagrams/50-cancellation-two-roads.svg new file mode 100644 index 0000000..dc7181a --- /dev/null +++ b/assets/diagrams/50-cancellation-two-roads.svg @@ -0,0 +1,51 @@ + + + +pthread_cancel — asynchronous, and it unwinds + +std::jthread + stop_token — cooperative, C++20's answer + +two roads out of a running thread + +worker parked +on a cancellation point +sleep(3), read(2), … + +pthread_cancel(t) +glibc throws a forced unwind +through the target's stack + +RAII really runs +~Guard() ran +cleanup handler ran +join → PTHREAD_CANCELED + +better than +its reputation: +destructors are +not skipped + +…but catch (...) without a rethrow swallows it +FATAL: exception not rethrown → SIGABRT, exit 134 +ordinary defensive C++ turns a cancel into a dead process + +request_stop() +sets a flag +no exception, no unwind + +stop_token::stop_requested() +the target checks +when IT chooses to + +the target decides where it is safe to stop +no cancellation points, no forced unwind, +nothing a catch-all can accidentally swallow + + + + + +Both rows are real runs on this host (glibc 2.43). The upper row is why pthread_cancel is powerful; the dashed box is why C++ did not standardize it. +The trap is not that cancellation skips cleanup — it does not. The trap is that the mechanism is an exception the caller must be careful never to catch. +Both cancel cases are hard-gated in verify.lua, including the abort: exit 134 with 'exception not rethrown' on stderr. + \ No newline at end of file diff --git a/assets/diagrams/50-thread-two-identities.excalidraw b/assets/diagrams/50-thread-two-identities.excalidraw new file mode 100644 index 0000000..a46e1b5 --- /dev/null +++ b/assets/diagrams/50-thread-two-identities.excalidraw @@ -0,0 +1,1702 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r109288042", + "type": "rectangle", + "x": 24, + "y": 54, + "width": 1112, + "height": 214, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1743539262, + "versionNonce": 84342452, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1821870345", + "type": "text", + "x": 36, + "y": 60, + "width": 290.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": 1242703354, + "versionNonce": 1615558000, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "C++ view \u2014 portable, and deliberately narrow", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "C++ view \u2014 portable, and deliberately narrow", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r673014227", + "type": "rectangle", + "x": 24, + "y": 286, + "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": 915768851, + "versionNonce": 1521844267, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1764476369", + "type": "text", + "x": 36, + "y": 292, + "width": 396.00000000000006, + "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": 1186667930, + "versionNonce": 509724653, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Linux view \u2014 the same thread, as the kernel and glibc see it", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Linux view \u2014 the same thread, as the kernel and glibc see it", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1936738630", + "type": "rectangle", + "x": 430, + "y": 14, + "width": 300, + "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": 1887546651, + "versionNonce": 180671896, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1550159878", + "type": "text", + "x": 440, + "y": 22, + "width": 200.20000000000002, + "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": 1786811262, + "versionNonce": 975489436, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one thread, two identities", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one thread, two identities", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r595912803", + "type": "rectangle", + "x": 60, + "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": 1145848642, + "versionNonce": 626510301, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t95793983", + "type": "text", + "x": 70, + "y": 104, + "width": 84.7, + "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": 1069379165, + "versionNonce": 807734958, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::thread", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::thread", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1448790201", + "type": "text", + "x": 70, + "y": 124, + "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": 1651939386, + "versionNonce": 1884483312, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "id: std::thread::id (opaque)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "id: std::thread::id (opaque)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1636807120", + "type": "text", + "x": 70, + "y": 139, + "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": 1148848689, + "versionNonce": 806677639, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "join / detach / joinable", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "join / detach / joinable", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1827329718", + "type": "text", + "x": 70, + "y": 154, + "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": 1939886480, + "versionNonce": 24733927, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "hardware_concurrency()", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "hardware_concurrency()", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t363249042", + "type": "text", + "x": 70, + "y": 169, + "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": 673723636, + "versionNonce": 1256966453, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no name, no stack, no affinity", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no name, no stack, no affinity", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r184902711", + "type": "rectangle", + "x": 430, + "y": 96, + "width": 300, + "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": 525471673, + "versionNonce": 1841823927, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1649769722", + "type": "text", + "x": 440, + "y": 104, + "width": 115.50000000000001, + "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": 235609135, + "versionNonce": 535206990, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "native_handle()", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "native_handle()", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t562932392", + "type": "text", + "x": 440, + "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": 618450325, + "versionNonce": 371836235, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the sanctioned escape hatch", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the sanctioned escape hatch", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t552027872", + "type": "text", + "x": 440, + "y": 139, + "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": 1897805617, + "versionNonce": 1323954240, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "native_handle_type", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "native_handle_type", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1855652829", + "type": "text", + "x": 440, + "y": 154, + "width": 102.85000000000001, + "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": 489720583, + "versionNonce": 1715923680, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "IS pthread_t here", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "IS pthread_t here", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1629258950", + "type": "text", + "x": 440, + "y": 169, + "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": 952019968, + "versionNonce": 1651469379, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "(static_assert, compile time)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(static_assert, compile time)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r911112319", + "type": "rectangle", + "x": 800, + "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": 778508550, + "versionNonce": 746193063, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t759892730", + "type": "text", + "x": 810, + "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": 411004843, + "versionNonce": 39624213, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::jthread + stop_token", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::jthread + stop_token", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t474242380", + "type": "text", + "x": 810, + "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": 484072697, + "versionNonce": 861068811, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cooperative cancellation:", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cooperative cancellation:", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1196190025", + "type": "text", + "x": 810, + "y": 139, + "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": 14116078, + "versionNonce": 324962760, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "a flag the target checks", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "a flag the target checks", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1855278770", + "type": "text", + "x": 810, + "y": 154, + "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": 1604515963, + "versionNonce": 1652629638, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no unwinding, no exception", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no unwinding, no exception", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t347922790", + "type": "text", + "x": 810, + "y": 169, + "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": 1572158018, + "versionNonce": 1999373397, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the answer to pthread_cancel", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the answer to pthread_cancel", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r326083339", + "type": "rectangle", + "x": 60, + "y": 330, + "width": 250, + "height": 126, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 456906792, + "versionNonce": 1769837264, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1855980297", + "type": "text", + "x": 70, + "y": 338, + "width": 69.30000000000001, + "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": 1316024004, + "versionNonce": 1729352369, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pthread_t", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pthread_t", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t660398429", + "type": "text", + "x": 70, + "y": 358, + "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": 305232029, + "versionNonce": 2083649, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "an opaque glibc handle", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "an opaque glibc handle", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t296427176", + "type": "text", + "x": 70, + "y": 373, + "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": 1397623193, + "versionNonce": 115456145, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "compare with pthread_equal", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "compare with pthread_equal", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1627108680", + "type": "text", + "x": 70, + "y": 388, + "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": 1391876141, + "versionNonce": 685930147, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "NOT a number, not a tid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "NOT a number, not a tid", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r429296818", + "type": "rectangle", + "x": 345, + "y": 330, + "width": 250, + "height": 126, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 689362140, + "versionNonce": 69487999, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t49287252", + "type": "text", + "x": 355, + "y": 338, + "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": 1279765687, + "versionNonce": 1631851904, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "gettid() \u2014 the kernel tid", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "gettid() \u2014 the kernel tid", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1784368159", + "type": "text", + "x": 355, + "y": 358, + "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": 1393856648, + "versionNonce": 1915137464, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "a real integer; names a dir", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "a real integer; names a dir", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1181711525", + "type": "text", + "x": 355, + "y": 373, + "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": 568039994, + "versionNonce": 219538871, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "/proc/self/task//", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "/proc/self/task//", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t925599026", + "type": "text", + "x": 355, + "y": 388, + "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": 1834124328, + "versionNonce": 993721616, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "main thread: tid == pid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "main thread: tid == pid", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1618387446", + "type": "rectangle", + "x": 630, + "y": 330, + "width": 470, + "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": 538151804, + "versionNonce": 160238553, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t977155415", + "type": "text", + "x": 640, + "y": 338, + "width": 207.9, + "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": 437908668, + "versionNonce": 1697531588, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "naming \u2014 pthread_setname_np", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "naming \u2014 pthread_setname_np", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t355506094", + "type": "text", + "x": 640, + "y": 358, + "width": 302.5, + "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": 1318926406, + "versionNonce": 1144080257, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\u2192 /proc/self/task//comm \u00b7 15 max, 16 \u2192 ERANGE", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\u2192 /proc/self/task//comm \u00b7 15 max, 16 \u2192 ERANGE", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1761816603", + "type": "rectangle", + "x": 630, + "y": 402, + "width": 470, + "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": 1163635695, + "versionNonce": 1697716378, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1558753351", + "type": "text", + "x": 640, + "y": 410, + "width": 254.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": 1809193085, + "versionNonce": 592275485, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stack \u2014 pthread_attr_setstacksize", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stack \u2014 pthread_attr_setstacksize", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t198756998", + "type": "text", + "x": 640, + "y": 430, + "width": 296.45000000000005, + "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": 1411090238, + "versionNonce": 1952349961, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\u2192 262144 \u00b7 below PTHREAD_STACK_MIN=16384 \u2192 EINVAL", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\u2192 262144 \u00b7 below PTHREAD_STACK_MIN=16384 \u2192 EINVAL", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r639382304", + "type": "rectangle", + "x": 630, + "y": 474, + "width": 470, + "height": 42, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 472682073, + "versionNonce": 989107947, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t479820234", + "type": "text", + "x": 640, + "y": 482, + "width": 454.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": 501536169, + "versionNonce": 1456539566, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "affinity \u2014 one THREAD \u00b7 sched \u2014 SCHED_OTHER/0, FIFO \u2192 EPERM", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "affinity \u2014 one THREAD \u00b7 sched \u2014 SCHED_OTHER/0, FIFO \u2192 EPERM", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r8649728", + "type": "rectangle", + "x": 60, + "y": 474, + "width": 535, + "height": 42, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1059865797, + "versionNonce": 1247672935, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1919343910", + "type": "text", + "x": 70, + "y": 482, + "width": 392.70000000000005, + "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": 1784066961, + "versionNonce": 1818062526, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "every facet still reports digest=0x481984990deee5ff", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "every facet still reports digest=0x481984990deee5ff", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "a834904153", + "type": "arrow", + "x": 360, + "y": 166, + "width": 70, + "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": 472165469, + "versionNonce": 729819220, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 70, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a498973499", + "type": "arrow", + "x": 560, + "y": 236, + "width": 90, + "height": 94, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1091842808, + "versionNonce": 1383228962, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -90, + 94 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a820921788", + "type": "arrow", + "x": 730, + "y": 166, + "width": 70, + "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": 1568797075, + "versionNonce": 382031364, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 70, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1878400557", + "type": "text", + "x": 40, + "y": 556, + "width": 1064.8000000000002, + "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": 109801189, + "versionNonce": 858865582, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::thread wraps a pthread and exposes almost none of it. Everything in the lower band is reachable, but only through native_handle() \u2014 which is why the standard put it there.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::thread wraps a pthread and exposes almost none of it. Everything in the lower band is reachable, but only through native_handle() \u2014 which is why the standard put it there.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t374319591", + "type": "text", + "x": 40, + "y": 580, + "width": 1028.5, + "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": 934403202, + "versionNonce": 428661582, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pthread_t and gettid() are not two spellings of one id: one is a glibc handle you may only compare, the other is the kernel's task id that /proc, perf, and gdb all speak.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pthread_t and gettid() are not two spellings of one id: one is a glibc handle you may only compare, the other is the kernel's task id that /proc, perf, and gdb all speak.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t639315201", + "type": "text", + "x": 40, + "y": 604, + "width": 707.85, + "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": 1524325868, + "versionNonce": 1383587064, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Every value shown is from a real run on the 16-CPU reference host (glibc 2.43). Nothing here is a duration; see ch39.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Every value shown is from a real run on the 16-CPU reference host (glibc 2.43). Nothing here is a duration; see ch39.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/50-thread-two-identities.svg b/assets/diagrams/50-thread-two-identities.svg new file mode 100644 index 0000000..11dfe92 --- /dev/null +++ b/assets/diagrams/50-thread-two-identities.svg @@ -0,0 +1,53 @@ + + + +C++ view — portable, and deliberately narrow + +Linux view — the same thread, as the kernel and glibc see it + +one thread, two identities + +std::thread +id: std::thread::id (opaque) +join / detach / joinable +hardware_concurrency() +no name, no stack, no affinity + +native_handle() +the sanctioned escape hatch +native_handle_type +IS pthread_t here +(static_assert, compile time) + +std::jthread + stop_token +cooperative cancellation: +a flag the target checks +no unwinding, no exception +the answer to pthread_cancel + +pthread_t +an opaque glibc handle +compare with pthread_equal +NOT a number, not a tid + +gettid() — the kernel tid +a real integer; names a dir +/proc/self/task/<tid>/ +main thread: tid == pid + +naming — pthread_setname_np +→ /proc/self/task/<tid>/comm · 15 max, 16 → ERANGE + +stack — pthread_attr_setstacksize +→ 262144 · below PTHREAD_STACK_MIN=16384 → EINVAL + +affinity — one THREAD · sched — SCHED_OTHER/0, FIFO → EPERM + +every facet still reports digest=0x481984990deee5ff + + + +std::thread wraps a pthread and exposes almost none of it. Everything in the lower band is reachable, but only through native_handle() — which is why the standard put it there. +pthread_t and gettid() are not two spellings of one id: one is a glibc handle you may only compare, the other is the kernel's task id that /proc, perf, and gdb all speak. +Every value shown is from a real run on the 16-CPU reference host (glibc 2.43). Nothing here is a duration; see ch39. + \ No newline at end of file diff --git a/assets/diagrams/README.md b/assets/diagrams/README.md index 5a2dc0f..b2304a0 100644 --- a/assets/diagrams/README.md +++ b/assets/diagrams/README.md @@ -97,3 +97,5 @@ here as you add them: | `48-rust-tool-gates` | 48 | one crate fanning into two gate lanes by verification status -- the amber hard lane (rustup pin, digest, `cargo fmt --check`, `cargo clippy -D warnings` naming `needless_return`, `cargo test`) that runs offline from the pinned toolchain alone, beside the dashed gated-if-present lane (cargo-nextest, cargo-deny, cargo-llvm-cov, cargo-audit noted `advisory-db/network`, cargo-watch/cargo-flamegraph/sccache shown-not-gated) not verified on this host | | `49-concurrency-vs-parallelism` | 49 | the two independent axes as a 2x2 grid -- not-parallel and parallel bands crossed with sequential (`max_inflight=1`, `interleaves=0`), the amber `concurrent` cell (16 tasks pinned to one CPU via `sched_setaffinity`, `cpus_allowed=1`/`distinct_cpus=1`/`interleaves=19`), `parallel` (one task per CPU, `distinct_cpus=16`), and the amber `both` cell (32 tasks oversubscribed onto 16 CPUs, `max_inflight=25 > cpus_allowed=16`), all four converging on `digest=0x481984990deee5ff`, with side notes distinguishing `cpus_allowed` as a permission from `distinct_cpus` as an outcome | | `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 | diff --git a/examples/50-pthreads/README.md b/examples/50-pthreads/README.md new file mode 100644 index 0000000..80da05d --- /dev/null +++ b/examples/50-pthreads/README.md @@ -0,0 +1,128 @@ +# 50 — pthreads + +Chapter 49 measured threads from above, with `std::thread`. This example goes +**under** `std::thread` to the thing it is on Linux: a pthread. Each subcommand +exercises one per-thread control that POSIX offers and the C++ standard library +does not expose, and reports something the kernel or glibc produced. + +``` +[host]$ ./demo.sh cpp run naming +naming: tid=2675834 via_pthread='ch50-worker' via_proc='ch50-worker' agree=yes +naming: 16char_rc=34 (ERANGE) name_after='ch50-worker' unchanged=yes +pthreads report: facet=naming digest=0x481984990deee5ff +``` + +## The facets + +| subcommand | control | observable | +| --- | --- | --- | +| `identity` | `gettid(2)` vs `pthread_t` vs `getpid(2)` | main's tid **equals** the pid; workers get distinct tids; each names a `/proc/self/task//` directory | +| `naming` | `pthread_setname_np` | the name read back from **both** `pthread_getname_np` and `/proc/self/task//comm`; a 16-char name refused with `ERANGE` | +| `stack` | `pthread_attr_setstacksize` | `pthread_getattr_np` reports exactly 262144 for a 256 KiB request vs 8388608 by default; a sub-`PTHREAD_STACK_MIN` request refused with `EINVAL` | +| `affinity` | `pthread_setaffinity_np` | two threads pinned to two CPUs report two different `sched_getcpu()` values, and the caller's own mask is untouched | +| `sched` | `pthread_getschedparam` / `setschedparam` | default is `SCHED_OTHER` at priority 0; an unprivileged `SCHED_FIFO` request is refused with `EPERM` | +| `cancel` | `pthread_cancel` | the C++ destructor **and** the `pthread_cleanup_push` handler both run; `pthread_join` reports `PTHREAD_CANCELED` | +| `cancel-swallow` | the same, mishandled | a `catch (...)` that does not rethrow aborts the process: `FATAL: exception not rethrown`, exit 134 | +| `bridge` | `std::thread::native_handle()` | naming a `std::thread` from outside lands in that thread's procfs `comm` — `native_handle_type` **is** `pthread_t`, checked by `static_assert` | + +One subcommand per facet is deliberate: `cancel-swallow` ends in `abort()` by +design, and a facet that aborts must never be able to take the others with it. + +Every facet prints `digest=0x481984990deee5ff` over the payload +`The quick brown.` — the same value ch46's C++, ch47's Go, ch48's Rust, and +ch49's `conc` produce. As always, the answer does not depend on how the work +was scheduled, or here on which per-thread knob was turned. + +## Two identities, and why it matters + +A Linux thread has two names and they are not interchangeable: + +- **`pthread_t`** is an opaque glibc handle. You may compare it with + `pthread_equal` and nothing else. It is not a number, and printing it is not + portable. +- **`gettid()`** is the kernel's task id: a real integer, the thing that names + a directory in `/proc`, the thing `perf`, `ftrace`, `top -H`, and gdb all + report. + +The main thread is the case that catches people out — its tid *equals* the +process pid, because a process on Linux is its first thread. + +## Layout + +``` +50-pthreads/ +├── demo.sh # dispatcher (C++ only) +├── verify.lua # gates A-H hard, I skip-if-present, E skip-if-1-CPU +└── cpp/ + ├── CMakeLists.txt + ├── CMakePresets.json + ├── demo.sh + └── src/pthreads.cpp +``` + +C++-only, like ch49. Go and Rust wrap these same pthreads and deliberately hide +exactly these controls, so a three-language version would have two directories +with nothing to say. `verify.lua` skips for any `LSP_LANG` other than `cpp`. + +Standard library and pthreads only. Local, unprivileged, offline — **no Boost, +no Conan, no network, no VM, no root**. + +## Build and run + +``` +[host]$ ./demo.sh cpp build +[host]$ ./demo.sh cpp run identity +[host]$ ./demo.sh cpp run naming +[host]$ ./demo.sh cpp run stack +[host]$ ./demo.sh cpp run affinity +[host]$ ./demo.sh cpp run sched +[host]$ ./demo.sh cpp run cancel +[host]$ ./demo.sh cpp run bridge +[host]$ ./demo.sh cpp run cancel-swallow # aborts on purpose, exit 134 +``` + +Bare `./demo.sh` builds and runs `identity`. `TARGET=` deploys to a lab VM, +though nothing here needs one. + +The facets are short-lived by design, but the `cancel` one parks its worker on +a cancellation point for ~200 ms, which is long enough to catch from another +terminal: + +``` +[host]$ ./cpp/build/release/pthreads cancel & sleep 0.1 +[host]$ ps -L -o pid,tid,comm,psr -p "$(pgrep -x pthreads)" + PID TID COMMAND PSR +2676217 2676217 pthreads 2 +2676217 2676219 pthreads 14 +``` + +Two rows for one process: `TID` is the same integer `gettid()` returns, and the +first row's TID equals the PID because a process is its first thread. `PSR` is +the CPU each thread is on — the `affinity` facet is what makes those two numbers +something you chose rather than something the scheduler picked. `COMMAND` is the +procfs `comm` field, which defaults to the process name; the `naming` and +`bridge` facets are the ones that change it. + +## Verification + +``` +[host]$ LSP_LANG=cpp lua verify.lua +``` + +Gates A–H are hard and all run offline and unprivileged: + +- **A** — every facet computes the expected digest. +- **B** — identity: main tid == pid, worker tids distinct, `/proc/self/task/` present. +- **C** — naming: both read-back paths agree, and 16 chars is refused with `ERANGE` leaving the old name in place. +- **D** — stack: the custom thread got at least what was asked and less than the default; a sub-minimum request is refused with `EINVAL` rather than rounded up. +- **E** — affinity: two threads on two different CPUs, caller's mask unchanged. Prints an informational `SKIP` (never a FAIL) on a host allowed fewer than 2 CPUs. +- **F** — sched: `SCHED_OTHER` at priority 0, and `SCHED_FIFO` refused with `EPERM`. **The refusal is the gate** — asserting a *successful* `SCHED_FIFO` would need root and would not run on a reader's laptop. +- **G** — cancel, both directions: the unwind runs destructors and cleanup handlers, and swallowing that unwind kills the process with `exception not rethrown`. +- **H** — bridge: `native_handle_type` is `pthread_t`, and a `std::thread` named through it shows that name in procfs. + +**I** (clang parity on the digest) runs if `clang++` is on `PATH` and prints an +informational `SKIP` otherwise. + +Not gated: a *successful* `SCHED_FIFO` change, which needs `CAP_SYS_NICE` or an +`RLIMIT_RTPRIO` allowance. The `EPERM` above is what an unprivileged run can +prove, and `verify.lua` says so in its own output rather than staying quiet. diff --git a/examples/50-pthreads/cpp/.clang-format b/examples/50-pthreads/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/50-pthreads/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/50-pthreads/cpp/.clang-tidy b/examples/50-pthreads/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/50-pthreads/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/50-pthreads/cpp/CMakeLists.txt b/examples/50-pthreads/cpp/CMakeLists.txt new file mode 100644 index 0000000..278bf59 --- /dev/null +++ b/examples/50-pthreads/cpp/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.25) +project(pthreads CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# THREADS_PREFER_PTHREAD_FLAG makes this -pthread rather than -lpthread, which +# matters: -pthread also sets the preprocessor state glibc expects. On glibc +# 2.34+ the pthread symbols live in libc proper, but the flag is still the +# supported spelling and keeps the build correct on older systems. +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(pthreads src/pthreads.cpp) +target_link_libraries(pthreads PRIVATE Threads::Threads) diff --git a/examples/50-pthreads/cpp/CMakePresets.json b/examples/50-pthreads/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/50-pthreads/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/50-pthreads/cpp/demo.sh b/examples/50-pthreads/cpp/demo.sh new file mode 100755 index 0000000..2b4e545 --- /dev/null +++ b/examples/50-pthreads/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/pthreads + +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 identity + ;; + *) + echo "usage: $0 [build|run ]" >&2 + exit 2 + ;; +esac diff --git a/examples/50-pthreads/cpp/src/pthreads.cpp b/examples/50-pthreads/cpp/src/pthreads.cpp new file mode 100644 index 0000000..5cb8e60 --- /dev/null +++ b/examples/50-pthreads/cpp/src/pthreads.cpp @@ -0,0 +1,470 @@ +// pthreads -- the reference program for ch50. +// +// ch49 measured concurrency and parallelism through std::thread. This program +// goes under std::thread to the thing it actually is on Linux: a pthread. Each +// subcommand exercises one per-thread control that POSIX offers and the C++ +// standard library does not expose, and reports an observable the kernel or +// glibc produced: +// +// identity pthread_t vs gettid() vs getpid(), and /proc/self/task/ +// naming pthread_setname_np -> /proc/self/task//comm +// stack pthread_attr_setstacksize -> pthread_getattr_np +// affinity pthread_setaffinity_np, per THREAD (ch49 pinned the process) +// sched pthread_getschedparam, and what SCHED_FIFO costs unprivileged +// cancel pthread_cancel, C++ unwinding, and why stop_token exists +// +// One subcommand per facet, deliberately: the cancel facet's second case ends +// in abort(), and a facet that aborts must never be able to take the others +// with it. +// +// Every facet prints the same digest. As in ch46-ch49, the answer does not +// depend on how the work was scheduled -- or, here, on which per-thread knob +// was turned. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +// ch46/ch47/ch48/ch49's exact 16-byte payload ("The quick brown."), carried +// forward so every chapter in the book lands on the same FNV-1a digest. +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* facet) { + std::printf("pthreads report: facet=%s digest=0x%016llx\n", facet, + static_cast(fnv1a(kPayload, sizeof kPayload))); +} + +// The kernel's name for a task, straight from procfs. This is the same string +// `top -H`, `ps -L`, and gdb's `info threads` display -- reading it here proves +// the name reached the kernel rather than living in a glibc-side variable. +std::string comm_of(pid_t tid) { + std::ifstream f("/proc/self/task/" + std::to_string(tid) + "/comm"); + std::string s; + std::getline(f, s); + return s; +} + +bool task_dir_exists(pid_t tid) { + return access(("/proc/self/task/" + std::to_string(tid)).c_str(), F_OK) == 0; +} + +int cpus_allowed() { + cpu_set_t set; + CPU_ZERO(&set); + if (sched_getaffinity(0, sizeof set, &set) != 0) { + return -1; + } + return CPU_COUNT(&set); +} + +// ── identity ───────────────────────────────────────────────────────────── +// +// A Linux thread has two identities and they are not interchangeable. +// pthread_t is an opaque glibc handle -- you may compare it with +// pthread_equal and nothing else; it is not a number and printing it is not +// portable. gettid() is the kernel's task id: a real integer, the thing that +// names a directory in /proc, the thing perf and ftrace and gdb report. +// +// The main thread is the case that surprises people: its tid EQUALS the +// process's pid, because a "process" on Linux is its first thread. + +void facet_identity() { + std::printf("identity: main pid=%ld tid=%ld same=%s\n", static_cast(getpid()), + static_cast(gettid()), getpid() == gettid() ? "yes" : "no"); + + constexpr int kWorkers = 3; + std::vector tids(kWorkers, 0); + std::vector threads; + for (int i = 0; i < kWorkers; ++i) { + threads.emplace_back([&tids, i] { tids[static_cast(i)] = gettid(); }); + } + for (auto& t : threads) { + t.join(); + } + + // Distinct from each other and from the process -- and each one named a + // real directory under /proc/self/task while it was alive. + bool distinct = true; + for (int i = 0; i < kWorkers; ++i) { + if (tids[static_cast(i)] == getpid()) { + distinct = false; + } + for (int j = i + 1; j < kWorkers; ++j) { + if (tids[static_cast(i)] == tids[static_cast(j)]) { + distinct = false; + } + } + std::printf("identity: worker %d tid=%ld\n", i, static_cast(tids[static_cast(i)])); + } + std::printf("identity: worker_tids_distinct=%s main_task_dir=%s\n", distinct ? "yes" : "no", + task_dir_exists(gettid()) ? "present" : "absent"); + report("identity"); +} + +// ── naming ─────────────────────────────────────────────────────────────── +// +// std::thread has no name. pthread_setname_np does, and the name it sets is +// not a glibc bookkeeping detail: it lands in the kernel's task_struct and +// shows up in /proc/self/task//comm, in `top -H`, and in gdb. +// +// The size limit is the interesting part. The kernel's comm field is 16 bytes +// INCLUDING the terminating NUL, so 15 characters is the most that fits. +// glibc refuses a longer name with ERANGE rather than truncating it, which +// means an over-long name leaves the previous one in place -- a silent no-op +// if you do not check the return value. + +void facet_naming() { + pid_t tid = 0; + std::string via_pthread; + std::string via_proc; + int too_long_rc = 0; + std::string after_too_long; + + std::thread t([&] { + tid = gettid(); + pthread_setname_np(pthread_self(), "ch50-worker"); + + char buf[32] = {}; + pthread_getname_np(pthread_self(), buf, sizeof buf); + via_pthread = buf; + via_proc = comm_of(tid); + + // 16 characters: one too many for a 16-byte field that must hold a NUL. + too_long_rc = pthread_setname_np(pthread_self(), "0123456789abcdef"); + after_too_long = comm_of(tid); + }); + t.join(); + + std::printf("naming: tid=%ld via_pthread='%s' via_proc='%s' agree=%s\n", static_cast(tid), + via_pthread.c_str(), via_proc.c_str(), via_pthread == via_proc ? "yes" : "no"); + std::printf("naming: 16char_rc=%d (%s) name_after='%s' unchanged=%s\n", too_long_rc, + too_long_rc == ERANGE ? "ERANGE" : std::strerror(too_long_rc), after_too_long.c_str(), + after_too_long == via_proc ? "yes" : "no"); + report("naming"); +} + +// ── stack ──────────────────────────────────────────────────────────────── +// +// std::thread gives you no say in stack size whatsoever. You get the default, +// which on glibc is RLIMIT_STACK for the main thread and a flat 8 MiB for +// every thread it creates. For a program that spawns thousands of threads +// that default is the difference between fitting in memory and not. +// +// pthread_attr_setstacksize sets it, pthread_getattr_np reads back what was +// actually installed, and a request below PTHREAD_STACK_MIN is refused with +// EINVAL rather than quietly rounded up. + +std::size_t observed_stack_size() { + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) != 0) { + return 0; + } + void* base = nullptr; + std::size_t size = 0; + pthread_attr_getstack(&attr, &base, &size); + pthread_attr_destroy(&attr); + return size; +} + +void* stack_probe(void* out) { + *static_cast(out) = observed_stack_size(); + return nullptr; +} + +void facet_stack() { + constexpr std::size_t kRequested = 256 * 1024; + + std::size_t default_size = 0; + pthread_t d{}; + pthread_create(&d, nullptr, stack_probe, &default_size); + pthread_join(d, nullptr); + + std::size_t custom_size = 0; + pthread_attr_t attr; + pthread_attr_init(&attr); + const int set_rc = pthread_attr_setstacksize(&attr, kRequested); + pthread_t c{}; + pthread_create(&c, &attr, stack_probe, &custom_size); + pthread_join(c, nullptr); + pthread_attr_destroy(&attr); + + // Below the floor: refused, not rounded. + pthread_attr_t small; + pthread_attr_init(&small); + const int too_small_rc = pthread_attr_setstacksize(&small, 8192); + pthread_attr_destroy(&small); + + std::printf("stack: main=%zu default_worker=%zu custom=%zu requested=%zu set_rc=%d\n", + observed_stack_size(), default_size, custom_size, kRequested, set_rc); + std::printf("stack: custom_smaller_than_default=%s custom_at_least_requested=%s\n", + custom_size < default_size ? "yes" : "no", custom_size >= kRequested ? "yes" : "no"); + std::printf("stack: PTHREAD_STACK_MIN=%ld below_min_rc=%d (%s)\n", static_cast(PTHREAD_STACK_MIN), + too_small_rc, too_small_rc == EINVAL ? "EINVAL" : std::strerror(too_small_rc)); + report("stack"); +} + +// ── affinity ───────────────────────────────────────────────────────────── +// +// ch49 called sched_setaffinity(0, ...) and moved the WHOLE PROCESS onto one +// CPU. pthread_setaffinity_np moves ONE THREAD, which is the control you +// actually want for a pinned fast path beside unpinned background work +// (ch40's territory). The observable difference: two workers pinned to two +// different CPUs report two different sched_getcpu() values, while the main +// thread's own affinity mask is left exactly as it was. + +struct PinArg { + int cpu; + int observed; + int rc; +}; + +void* pin_probe(void* raw) { + auto* a = static_cast(raw); + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(static_cast(a->cpu), &set); + a->rc = pthread_setaffinity_np(pthread_self(), sizeof set, &set); + // sched_yield gives the kernel an obvious moment to honour the new mask; + // it is not required for correctness, only for a prompt observation. + sched_yield(); + a->observed = sched_getcpu(); + return nullptr; +} + +void facet_affinity() { + const int allowed_before = cpus_allowed(); + if (allowed_before < 2) { + std::printf("affinity: cpus_allowed=%d -- need at least 2 to pin two threads apart\n", + allowed_before); + report("affinity"); + return; + } + + PinArg a{0, -1, -1}; + PinArg b{1, -1, -1}; + pthread_t ta{}; + pthread_t tb{}; + pthread_create(&ta, nullptr, pin_probe, &a); + pthread_create(&tb, nullptr, pin_probe, &b); + pthread_join(ta, nullptr); + pthread_join(tb, nullptr); + + const int allowed_after = cpus_allowed(); + std::printf("affinity: worker_a asked=%d rc=%d observed_cpu=%d\n", a.cpu, a.rc, a.observed); + std::printf("affinity: worker_b asked=%d rc=%d observed_cpu=%d\n", b.cpu, b.rc, b.observed); + std::printf("affinity: workers_on_different_cpus=%s main_cpus_allowed=%d unchanged=%s\n", + (a.observed != b.observed && a.observed >= 0 && b.observed >= 0) ? "yes" : "no", + allowed_after, allowed_before == allowed_after ? "yes" : "no"); + report("affinity"); +} + +// ── sched ──────────────────────────────────────────────────────────────── +// +// Scheduling policy is per-thread and std::thread cannot touch it. The +// default is SCHED_OTHER at priority 0 -- the fair scheduler, where "priority" +// is nice and the sched_param priority is required to be zero. +// +// Asking for SCHED_FIFO is the interesting call precisely because it FAILS +// here: real-time policy needs CAP_SYS_NICE or an RLIMIT_RTPRIO allowance, and +// an unprivileged process gets EPERM. That refusal is the observable. A demo +// that required root to show anything would not run on a reader's laptop. + +void facet_sched() { + int policy = -1; + sched_param param{}; + const int get_rc = pthread_getschedparam(pthread_self(), &policy, ¶m); + + sched_param rt{}; + rt.sched_priority = 50; + const int fifo_rc = pthread_setschedparam(pthread_self(), SCHED_FIFO, &rt); + + int policy_after = -1; + sched_param after{}; + pthread_getschedparam(pthread_self(), &policy_after, &after); + + std::printf("sched: get_rc=%d policy=%d is_other=%s priority=%d\n", get_rc, policy, + policy == SCHED_OTHER ? "yes" : "no", param.sched_priority); + std::printf("sched: SCHED_FIFO prio 50 -> rc=%d (%s) policy_after=%d unchanged=%s\n", fifo_rc, + fifo_rc == EPERM ? "EPERM" : std::strerror(fifo_rc), policy_after, + policy == policy_after ? "yes" : "no"); + report("sched"); +} + +// ── cancel ─────────────────────────────────────────────────────────────── +// +// pthread_cancel is the facility C++ deliberately did NOT standardize, and +// this facet shows why by running it against C++ code. +// +// Case 1 is the part that works better than its reputation: on glibc, +// cancellation unwinds the stack, so C++ destructors DO run and +// pthread_cleanup_push handlers DO run. RAII is not silently skipped. +// +// Case 2 is the trap. The unwind is implemented as a special exception, and +// glibc requires it to propagate. A `catch (...)` that does not rethrow +// swallows it, and glibc responds by aborting the whole process with +// "FATAL: exception not rethrown". A perfectly ordinary, correct-looking C++ +// catch-all turns a thread cancellation into a dead process. +// +// C++20's std::jthread and std::stop_token exist to avoid all of this: a +// cooperative request the target checks when it chooses to, with no unwinding +// and no exception the caller must be careful not to catch. + +struct Guard { + const char* name; + ~Guard() { std::printf("cancel: ~Guard(%s) ran during unwinding\n", name); } +}; + +void cleanup_handler(void* arg) { + std::printf("cancel: pthread_cleanup handler ran (holding %s)\n", static_cast(arg)); + std::fflush(stdout); +} + +void* unwind_worker(void*) { + pthread_cleanup_push(cleanup_handler, const_cast("a-resource")); + Guard g{"raii"}; + std::printf("cancel: worker parked on a cancellation point\n"); + std::fflush(stdout); + for (;;) { + sleep(1); // sleep(3) is a cancellation point + } + pthread_cleanup_pop(1); + return nullptr; +} + +void* swallow_worker(void*) { + try { + std::printf("cancel: worker parked inside catch (...)\n"); + std::fflush(stdout); + for (;;) { + sleep(1); + } + } catch (...) { + // Looks like ordinary defensive C++. It is fatal here: the forced + // unwind must be allowed to propagate, and glibc aborts if it is not. + std::printf("cancel: caught the forced unwind and did not rethrow\n"); + std::fflush(stdout); + } + return nullptr; +} + +void facet_cancel(bool swallow) { + pthread_t t{}; + if (swallow) { + // This case ends in abort() by design. It is its own subcommand so it + // can never take the other facets down with it. + pthread_create(&t, nullptr, swallow_worker, nullptr); + usleep(200000); + pthread_cancel(t); + pthread_join(t, nullptr); + std::printf("cancel: still alive after swallowing the unwind\n"); + return; + } + + pthread_create(&t, nullptr, unwind_worker, nullptr); + usleep(200000); + const int rc = pthread_cancel(t); + void* retval = nullptr; + pthread_join(t, &retval); + std::printf("cancel: pthread_cancel rc=%d joined_retval_is_canceled=%s\n", rc, + retval == PTHREAD_CANCELED ? "yes" : "no"); + report("cancel"); +} + +// ── the bridge ─────────────────────────────────────────────────────────── +// +// Everything above used raw pthread_create, but none of it has to. A +// std::thread IS a pthread here -- native_handle_type is pthread_t, checked +// at compile time below -- so the standard type and the POSIX controls +// compose. This is the sanctioned escape hatch, and using it is not a hack: +// native_handle() exists in the standard precisely so that platform facilities +// the standard does not wrap remain reachable. + +static_assert(std::is_same_v, + "on this platform std::thread::native_handle() is a pthread_t"); + +void facet_bridge() { + pid_t tid = 0; + std::string name; + std::atomic named{false}; + + std::thread t([&] { + tid = gettid(); + while (!named.load(std::memory_order_acquire)) { + sched_yield(); + } + name = comm_of(gettid()); + }); + + // Name a std::thread from the outside, through its native handle. + const int rc = pthread_setname_np(t.native_handle(), "std-thread-x"); + named.store(true, std::memory_order_release); + t.join(); + + std::printf("bridge: native_handle_is_pthread_t=yes setname_rc=%d tid=%ld proc_comm='%s'\n", rc, + static_cast(tid), name.c_str()); + report("bridge"); +} + +void usage() { + std::fprintf(stderr, + "usage: pthreads \n"); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 2) { + usage(); + return 2; + } + const std::string facet = argv[1]; + + if (facet == "identity") { + facet_identity(); + } else if (facet == "naming") { + facet_naming(); + } else if (facet == "stack") { + facet_stack(); + } else if (facet == "affinity") { + facet_affinity(); + } else if (facet == "sched") { + facet_sched(); + } else if (facet == "cancel") { + facet_cancel(false); + } else if (facet == "cancel-swallow") { + facet_cancel(true); + } else if (facet == "bridge") { + facet_bridge(); + } else { + usage(); + return 2; + } + return 0; +} diff --git a/examples/50-pthreads/demo.sh b/examples/50-pthreads/demo.sh new file mode 100755 index 0000000..a877a44 --- /dev/null +++ b/examples/50-pthreads/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +# Single-language example (C++ only), like ch49. This chapter is about what +# POSIX threads expose that std::thread does not; Go and Rust wrap the same +# pthreads and deliberately hide exactly these controls, so a three-language +# version would have two directories with nothing to say. +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/50-pthreads/verify.lua b/examples/50-pthreads/verify.lua new file mode 100644 index 0000000..66743f6 --- /dev/null +++ b/examples/50-pthreads/verify.lua @@ -0,0 +1,202 @@ +-- Verify 50-pthreads (C++ only): what POSIX threads expose that std::thread +-- does not, and what each control does that the kernel or glibc can be asked +-- to confirm. ch49 measured threads from above with std::thread; this chapter +-- goes under it to the pthread, and every assertion below checks a real, +-- program-observed effect -- a tid, a procfs string, an installed stack size, +-- a CPU id, a scheduling policy, an errno, or an abort message. +-- +-- NOTHING HERE IS TIMED (see ch39). Nothing here needs root, a VM, a network, +-- or a second host. +-- +-- Hard-gated (A-G): +-- A. Every facet computes the same digest, 0x481984990deee5ff -- the value +-- ch46's C++, ch47's Go, ch48's Rust, and ch49's conc all produce over +-- the same 16 payload bytes. +-- B. identity: the main thread's tid EQUALS the process pid (a process is +-- its first thread), worker tids are distinct from it and each other, +-- and /proc/self/task/ exists. +-- C. naming: pthread_setname_np's name is readable back through BOTH +-- pthread_getname_np and /proc/self/task//comm, and they agree; a +-- 16-character name is refused with ERANGE and leaves the old name in +-- place (the kernel's comm field is 16 bytes INCLUDING the NUL). +-- D. stack: a thread created with a 256 KiB attr observes at least that +-- much and less than the 8 MiB default, and a request below +-- PTHREAD_STACK_MIN is refused with EINVAL rather than rounded up. +-- E. affinity: two threads pinned with pthread_setaffinity_np report two +-- DIFFERENT sched_getcpu() values while the calling thread's own mask is +-- unchanged. This is the ch49 contrast -- that chapter moved the whole +-- process, this one moves one thread. Skipped with an informational +-- SKIP (never a FAIL, never a false PASS) on a host allowed <2 CPUs. +-- F. sched: the default policy is SCHED_OTHER at priority 0, and an +-- unprivileged SCHED_FIFO request is refused with EPERM leaving the +-- policy unchanged. The REFUSAL is the gate -- asserting a successful +-- SCHED_FIFO would require root and would not run on a reader's laptop. +-- G. cancel, both directions: +-- - pthread_cancel unwinds: the C++ destructor AND the +-- pthread_cleanup_push handler both run, and pthread_join reports +-- PTHREAD_CANCELED. +-- - swallowing that unwind with a catch (...) that does not rethrow +-- aborts the whole process with "exception not rethrown". This is +-- why C++20 standardized cooperative std::stop_token instead. +-- H. bridge: std::thread::native_handle() is a pthread_t (checked by +-- static_assert at compile time) and naming a std::thread through it +-- lands in that thread's procfs comm. +-- +-- Gated-if-present (I): clang++ parity on the digest, as ch49's gate G. + +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("50-pthreads is C++-only (LSP_LANG=" .. tostring(lang) .. ")") +end + +-- Escape a literal string for use inside a Lua pattern. +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 + +-- Pull a named integer field out of a facet's output. +local function field(out, name) + local v = out:match(name .. "=(%-?%d+)") + return v and tonumber(v) or nil +end + +local EXPECTED_DIGEST = "digest=0x481984990deee5ff" + +local b = checks.run("./demo.sh cpp build") +checks.expect_exit(b, 0, "cpp: builds (stdlib + pthreads only, no Boost, no network)") + +-- ── A. one digest, every facet ─────────────────────────────────────────── + +local facets = { "identity", "naming", "stack", "affinity", "sched", "cancel", "bridge" } +local out = {} +for _, f in ipairs(facets) do + local r = checks.run("./demo.sh cpp run " .. f) + checks.expect_exit(r, 0, "cpp: facet " .. f .. " runs") + checks.expect_match(r.out, lit(EXPECTED_DIGEST), + "cpp: facet " .. f .. " computes the expected digest (== ch46/47/48/49's)") + out[f] = r.out +end + +-- ── B. identity: pthread_t is not a tid, and a process is its first thread ─ + +checks.expect_match(out["identity"], "same=yes", + "cpp: the main thread's tid equals the process pid -- a process is its first thread") +checks.expect_match(out["identity"], "worker_tids_distinct=yes", + "cpp: every worker got a distinct kernel tid, none of them the pid") +checks.expect_match(out["identity"], "main_task_dir=present", + "cpp: the tid names a real directory under /proc/self/task") + +-- ── C. naming: the name reaches the kernel, and 16 chars is one too many ── + +checks.expect_match(out["naming"], "agree=yes", + "cpp: pthread_getname_np and /proc/self/task//comm report the same name") +checks.expect_match(out["naming"], "via_proc='ch50%-worker'", + "cpp: the name set with pthread_setname_np is the one procfs reports") +checks.expect_match(out["naming"], "16char_rc=34 %(ERANGE%)", + "cpp: a 16-character name is refused with ERANGE (comm is 16 bytes incl. NUL)") +checks.expect_match(out["naming"], "unchanged=yes", + "cpp: the refused rename left the previous name in place -- a silent no-op if unchecked") + +-- ── D. stack: std::thread has no say; pthreads does ─────────────────────── + +local st = out["stack"] +local custom = field(st, "custom") +local default_worker = field(st, "default_worker") +local requested = field(st, "requested") +checks.expect_match(st, "custom_at_least_requested=yes", + "cpp: the thread created with a 256 KiB attr got at least 256 KiB (custom=" .. + tostring(custom) .. ")") +checks.expect_match(st, "custom_smaller_than_default=yes", + "cpp: and materially less than the default (default_worker=" .. + tostring(default_worker) .. ")") +checks.expect_match(st, "below_min_rc=22 %(EINVAL%)", + "cpp: a stack below PTHREAD_STACK_MIN is refused with EINVAL, not rounded up") + +-- ── E. affinity: per THREAD, not per process (skip if <2 CPUs allowed) ──── + +local aff = out["affinity"] +local aff_allowed = field(aff, "main_cpus_allowed") +if aff:find("need at least 2 to pin two threads apart") then + print("SKIP: fewer than 2 CPUs allowed -- gate E not asserted") +else + checks.expect_match(aff, "workers_on_different_cpus=yes", + "cpp: two threads pinned with pthread_setaffinity_np ran on two different CPUs") + checks.expect_match(aff, "unchanged=yes", + "cpp: pinning those threads left the caller's own affinity mask alone " .. + "(main_cpus_allowed=" .. tostring(aff_allowed) .. ") -- unlike ch49's process-wide pin") + local rc_a = aff:match("worker_a asked=%d+ rc=(%-?%d+)") + checks.expect_match(tostring(rc_a), "^0$", + "cpp: pthread_setaffinity_np itself succeeded for worker A") +end + +-- ── F. sched: the refusal is the observable ────────────────────────────── + +checks.expect_match(out["sched"], "is_other=yes", + "cpp: the default scheduling policy is SCHED_OTHER") +checks.expect_match(out["sched"], "priority=0", + "cpp: SCHED_OTHER requires sched_priority 0 -- 'priority' there means nice") +checks.expect_match(out["sched"], "SCHED_FIFO prio 50 %-> rc=1 %(EPERM%)", + "cpp: an unprivileged SCHED_FIFO request is refused with EPERM") +checks.expect_match(out["sched"], "unchanged=yes", + "cpp: the refused policy change left the thread on SCHED_OTHER") + +-- ── G. cancel: it unwinds, and swallowing the unwind is fatal ───────────── + +checks.expect_match(out["cancel"], "~Guard%(raii%) ran during unwinding", + "cpp: pthread_cancel unwinds the stack -- the C++ destructor really ran") +checks.expect_match(out["cancel"], "pthread_cleanup handler ran", + "cpp: the pthread_cleanup_push handler ran too") +checks.expect_match(out["cancel"], "joined_retval_is_canceled=yes", + "cpp: pthread_join reported PTHREAD_CANCELED rather than a normal return") + +-- The second case aborts on purpose, which is why it is its own subcommand. +local swallow = checks.run("./demo.sh cpp run cancel-swallow 2>&1") +checks.expect_match(tostring(swallow.exit ~= 0), "true", + "cpp: swallowing the forced unwind kills the process (exit=" .. tostring(swallow.exit) .. ")") +checks.expect_match(swallow.out, "exception not rethrown", + "cpp: glibc names the reason -- a catch (...) that does not rethrow is fatal here") +checks.expect_match(swallow.out, "caught the forced unwind and did not rethrow", + "cpp: and the catch block really did run first, so this is not a crash before the catch") +checks.expect_match(tostring(swallow.out:find("still alive after swallowing") == nil), "true", + "cpp: execution never reached the line after the join -- the abort is not recoverable") + +-- ── H. the bridge: std::thread IS a pthread here ───────────────────────── + +checks.expect_match(out["bridge"], "native_handle_is_pthread_t=yes", + "cpp: std::thread::native_handle_type is pthread_t (static_assert, compile time)") +checks.expect_match(out["bridge"], "setname_rc=0", + "cpp: naming a std::thread through its native handle succeeded") +checks.expect_match(out["bridge"], "proc_comm='std%-thread%-x'", + "cpp: and procfs shows the name on the std::thread's own task") + +-- ── I. 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 pthreads >/dev/null 2>&1 && " .. + "./build/release-clang/pthreads identity") + checks.expect_exit(cl, 0, "cpp: clang-built pthreads runs") + checks.expect_match(cl.out, lit(EXPECTED_DIGEST), + "cpp: clang and GCC agree on the digest") + checks.expect_match(cl.out, "same=yes", + "cpp: and clang's build sees the same tid/pid identity") +else + print("SKIP: clang++ not found on PATH -- gate I not asserted") +end + +-- ── not gated: anything needing privilege ──────────────────────────────── + +print("info: a SUCCESSFUL SCHED_FIFO change is not gated -- it needs CAP_SYS_NICE or an" .. + " RLIMIT_RTPRIO allowance; the EPERM refusal above is what an unprivileged run can prove") + +checks.finish() diff --git a/examples/manifest.yaml b/examples/manifest.yaml index 74b415a..3dee585 100644 --- a/examples/manifest.yaml +++ b/examples/manifest.yaml @@ -205,3 +205,7 @@ examples: langs: [cpp] mode: local timeout: 600 + - id: 50-pthreads + langs: [cpp] + mode: local + timeout: 600