Part 14 8 chapters

Compendium: C++ Concurrency

A C++-only compendium that takes one workload through every concurrency model the language offers — pthreads, std threading, Boost.Thread, C++20 coroutines, Boost.Fiber, Boost.Asio. This compendium has been extracted into its own dedicated, runnable book, C++ Concurrency and Parallelism (patterncatalyst.github.io/cpp-concurrency-and-parallelism); the chapters here now point to it.

Chapter 49

Concurrency vs parallelism: one workload, four execution models, and observables that are counts rather than clocks

conc runs one identical workload under four execution models -- sequential, concurrent (pinned to one CPU by sched_setaffinity), parallel (one thread per CPU), and both (2x oversubscribed) -- and reports structure rather than time: cpus_allowed from sched_getaffinity as a permission, distinct_cpus unioned from sched_getcpu as an outcome, a max_inflight high-water mark, and interleaves counted from gaps in a monotonic ticket sequence. All four converge on digest=0x481984990deee5ff, the same value ch46's C++, ch47's Go, and ch48's Rust toolbox produce. Verified on the Fedora 44 host: verify.lua PASS 43/FAIL 0, concurrent reporting cpus_allowed=1 distinct_cpus=1 max_inflight=16 interleaves=16 while parallel reports distinct_cpus=16, C++26 Contracts built four times for four distinct behaviors under GCC 16, and a hardened build trapping on __n < this->size() where the unchecked build is asserted statically to contain no bounds check at all.

55 minutesRead →
Chapter 50

pthreads: the thread under std::thread, its two identities, and the six controls the standard does not give you

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.

55 minutesRead →
Chapter 51

The standard threading library: what a lock costs, and why the answer is zero until it isn't

stdthread performs the identical 200,000 increments three ways -- with no mutex, with an uncontended std::mutex, and with the same mutex across eight threads -- and counts the syscalls each one issues with strace. The uncontended case matches the no-mutex baseline exactly at 1 futex call, which is the dynamic loader rather than a lock, while the contended case costs 1844-3111. std::condition_variable, std::latch, std::barrier, std::counting_semaphore, and std::atomic::wait are then each shown reaching the same futex(2), and a std::lock_guard deadlock is reproduced under timeout(1) with every task's /proc wchan reading futex_do_wait, against a std::scoped_lock version that completes 100,000 acquisitions in opposing orders. Verified on the Fedora 44 host with glibc 2.43: verify.lua PASS 38/FAIL 0, local, unprivileged, offline.

60 minutesRead →
Chapter 52

Boost.Thread: what the standard took, what it left, and the third way to cancel a thread

boostthread asks what survived C++11's near-wholesale standardization of Boost.Thread, and answers with four capabilities std:: still lacks in C++23 on this toolchain: future::then() and when_all composing without blocking, where a concept evaluated against std::future and boost::future on every run reports no and yes; upgrade_lock promoting a reader to a writer in place, where std::upgrade_lock is not merely absent but undeclared; thread::interrupt() throwing an ordinary boost::thread_interrupted at defined interruption points; and thread::attributes::set_stack_size yielding exactly 262144, byte-identical to ch50's pthread_attr_setstacksize. The cancellation arc closes here -- catch (...) without a rethrow aborts the process under ch50's pthread_cancel (exit 134) and leaves it alive under Boost (exit 0), while a busy loop with no interruption point ignores interrupt() entirely. Verified against system Boost 1.90.0 on the Fedora 44 host: verify.lua PASS 37/FAIL 0, no Conan, no network.

55 minutesRead →
Chapter 53

C++20 coroutines: what a suspended computation costs, and the allocation that may not happen

coro measures what Chapter 27 built but never priced. A coroutine frame is measured by overloading operator new inside promise_type -- 32 bytes trivial, 304 carrying four ints and a char[256], 4128 carrying a char[4096] -- against the 8388608-byte thread stack Chapter 50 measured with pthread_getattr_np, a ratio of 262144x. Heap allocation elision turns out to be a permission rather than a guarantee: across 1000 calls to the easiest possible case, g++ 16.1.1 elides none at -O0 through -O3 while clang++ 22.1.8 elides every one from -O1 up, so verify.lua gates that the count is measured rather than that elision occurs. Plus C++23 std::generator exercised to the 20th Fibonacci term and the coroutine_handle lifetime trap counted through operator delete. Verified on the Fedora 44 host: verify.lua PASS 32/FAIL 0, C++23 stdlib only, no threads, no network.

55 minutesRead →
Chapter 54

Boost.Fiber: stackful suspension, the guard page, and the scheduler you have to choose

fiberdemo measures what a real stack costs and what it buys. boost::context::stack_traits reports a 131072-byte default fiber stack, sitting between ch53's measured 32-byte coroutine frame and ch50's measured 8388608-byte thread stack -- 8 MiB to 128 KiB to 32 bytes, with both ratios computed from measured numbers. The capability that stack buys is demonstrated rather than described: a fiber suspends from inside an ordinary non-coroutine function six frames deep and resumes there, which no stackless coroutine can do. protected_fixedsize_stack is shown costing exactly one page more than fixedsize_stack, gated against page_size(), and a 60x overflow dies of SIGSEGV on that guard page rather than corrupting the next mapping. The same 16 fibers report one distinct tid under round_robin and four under shared_work, making ch49's grid a scheduler choice. Verified against system Boost 1.90.0 on the Fedora 44 host: verify.lua PASS 32/FAIL 0, no Conan, no network.

55 minutesRead →
Chapter 55

Boost.Asio: the strand, the work guard, and who actually runs your handler

asiodemo measures what a library adds on top of the reactor Chapters 9, 22, and 27 built by hand -- and re-derives none of it. A freshly constructed io_context::run() returns after zero handlers, because it returns on no outstanding work rather than on completion. The same handler posted through a strand by 8 threads leaves a deliberately unsynchronized counter at exactly 2000 with max_inflight=1 and overlaps=0; posted straight to the io_context it overlaps 1492 times and loses increments for real -- a data race visible as a wrong number, whose magnitude is reported and never gated because it is UB. A strand and a std::mutex both produce the correct 20000, at 52 futex calls against 1022 counted with Chapter 51's strace method: a mutex blocks a thread, a strand defers a handler. Four buffers written as one sequence cost one sendmsg carrying 4 iovecs against four sendto, Part 5's writev machinery without an iovec array. Four shapes of run() report 1, 8, 4, and 4 distinct tids by gettid(). Cancellation arrives as a completion carrying operation_aborted -- a third shape after Chapter 51's flag and Chapter 52's exception. Verified against system Boost 1.90.0 on the Fedora 44 host: verify.lua PASS 48/FAIL 0, no Conan, no network, nothing bound to a port.

60 minutesRead →
Chapter 56

The capstone: one workload, seven models, and the two instruments that make them comparable

capstone closes Part 14 by fixing the flaw in every chapter before it: ch50-ch55 each measured one model, with its own instrument, on its own workload, so no two numbers in the compendium were strictly comparable. Here one workload -- 8 tasks x 5000 rounds folding into a single accumulator, shared verbatim by every arm -- runs through all six models plus a sequential control, and all of them produce the identical total 0xb75768f6610642a0, which is gated before any comparison is drawn. Measured with gettid(), the three single-threaded models hold eight tasks in flight on exactly one OS thread while the four multi-threaded ones report eight. Measured with ch51's strace method, the same 40000 folds cost 1 futex call under any single-threaded model, 230 to 451 under a lock, and 51 through an Asio strand. A seventh arm settles Chapter 49's conditional promise about P2300: __cpp_lib_senders is still undefined, so it links NVIDIA's stdexec reference implementation, pinned by an in-repo Conan recipe to tag nvhpc-26.05 by sha256, isolated so the six-model core needs no Conan and no network. Verified on the Fedora 44 host: verify.lua PASS 57/FAIL 0 with Conan present and PASS 52/FAIL 0 with it hidden.

65 minutesRead →