From e04a7a95527d58d38f65b52114fa9c524d2efcb8 Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 17 Aug 2026 10:23:27 -0400 Subject: [PATCH] =?UTF-8?q?docs(r19):=20pthreads=20=E2=80=94=20example=205?= =?UTF-8?q?0=20+=20chapter=2050=20(Compendium:=20C++=20Concurrency)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second chapter of Part 14. ch49 measured threads from above with std::thread; ch50 goes under it to the pthread and the per-thread controls the C++ standard library does not expose. Six facets, one subcommand each, every one reporting something the kernel or glibc produced: - identity — gettid() vs the opaque pthread_t vs getpid(); the main thread's tid equals the pid because a process is its first thread - naming — pthread_setname_np read back from both pthread_getname_np and /proc/self/task//comm; 16 chars refused with ERANGE, old name intact - stack — pthread_attr_setstacksize gives 262144 against an 8388608 default; below PTHREAD_STACK_MIN=16384 refused with EINVAL rather than rounded - affinity — pthread_setaffinity_np moves ONE thread, unlike ch49's process-wide sched_setaffinity(0, ...); caller's mask untouched - sched — SCHED_OTHER at priority 0; unprivileged SCHED_FIFO refused with EPERM. The refusal is the gate: a demo needing root would not run locally. - cancel — pthread_cancel DOES unwind (destructor and cleanup handler both run, join reports PTHREAD_CANCELED), but a catch (...) without a rethrow aborts the process with "FATAL: exception not rethrown". Both hard-gated. Plus the bridge: std::thread::native_handle_type is static_assert'd to be pthread_t, and naming a std::thread through it lands in its procfs comm. cancel-swallow is its own subcommand because it aborts by design and must never be able to take the other facets with it. Two diagrams (Fig 50.1 two identities, Fig 50.2 two roads out of a running thread), catalogued in assets/diagrams/README.md. Verified on the Fedora 44 host, glibc 2.43, local and unprivileged and offline: verify.lua PASS 45 / FAIL 0 with the affinity and clang-parity gates both running for real, runner 1 passed, validate.py OK. --- _docs/50-pthreads.md | 705 +++++++ _plans/r19-ch50-plan.md | 164 ++ .../50-cancellation-two-roads.excalidraw | 1655 ++++++++++++++++ assets/diagrams/50-cancellation-two-roads.svg | 51 + .../50-thread-two-identities.excalidraw | 1702 +++++++++++++++++ assets/diagrams/50-thread-two-identities.svg | 53 + assets/diagrams/README.md | 2 + examples/50-pthreads/README.md | 128 ++ examples/50-pthreads/cpp/.clang-format | 8 + examples/50-pthreads/cpp/.clang-tidy | 11 + examples/50-pthreads/cpp/CMakeLists.txt | 16 + examples/50-pthreads/cpp/CMakePresets.json | 62 + examples/50-pthreads/cpp/demo.sh | 40 + examples/50-pthreads/cpp/src/pthreads.cpp | 470 +++++ examples/50-pthreads/demo.sh | 26 + examples/50-pthreads/verify.lua | 202 ++ examples/manifest.yaml | 4 + 17 files changed, 5299 insertions(+) create mode 100644 _docs/50-pthreads.md create mode 100644 _plans/r19-ch50-plan.md create mode 100644 assets/diagrams/50-cancellation-two-roads.excalidraw create mode 100644 assets/diagrams/50-cancellation-two-roads.svg create mode 100644 assets/diagrams/50-thread-two-identities.excalidraw create mode 100644 assets/diagrams/50-thread-two-identities.svg create mode 100644 examples/50-pthreads/README.md create mode 100644 examples/50-pthreads/cpp/.clang-format create mode 100644 examples/50-pthreads/cpp/.clang-tidy create mode 100644 examples/50-pthreads/cpp/CMakeLists.txt create mode 100644 examples/50-pthreads/cpp/CMakePresets.json create mode 100755 examples/50-pthreads/cpp/demo.sh create mode 100644 examples/50-pthreads/cpp/src/pthreads.cpp create mode 100755 examples/50-pthreads/demo.sh create mode 100644 examples/50-pthreads/verify.lua 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