diff --git a/_docs/55-boost-asio.md b/_docs/55-boost-asio.md new file mode 100644 index 0000000..47cffe0 --- /dev/null +++ b/_docs/55-boost-asio.md @@ -0,0 +1,597 @@ +--- +title: "Boost.Asio: the strand, the work guard, and who actually runs your handler" +order: 55 +part: "Compendium: C++ Concurrency" +description: "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." +duration: "60 minutes" +--- + +Three chapters of this book have already built a reactor. Chapter 9 wrote the +epoll loop, level- and edge-triggered, with `EPOLLONESHOT` and timerfd and +signalfd. Chapter 22 wrote the nonblocking state machine around it and the +`EAGAIN` bookkeeping that makes partial reads and partial writes correct. +Chapter 27 drove a hand-rolled coroutine reactor over the same epoll fd. + +So the interesting question about Boost.Asio is not "how does it do readiness". +It does readiness the way you already know, on this platform through `epoll_wait` +in exactly the shape Chapter 9 built. The interesting question is what a library +has that a hand-written loop does not — and the answer turns out to be a +vocabulary for two things the hand-written loop never had to name, because it +only ever had one thread: **who runs a handler, and whether two of them can run +at once.** + +This chapter measures that vocabulary. Nothing below re-derives epoll, +readiness, or `EAGAIN`. + +{% include excalidraw.html + file="55-strand-not-a-mutex" + alt="The same handler, the same 8 threads and the same io_context posted two ways, and a lower band pricing the alternative. The upper left amber box, post(strand, handler), reports an unsynchronized counter reaching 2000 of 2000 and correct, with max_inflight equals 1 and overlaps equals 0, annotated that 8 threads ran handlers and no two ran at once. The upper right dashed ghost box, post(io_context, handler), reports the same counter reaching only 1862 of 2000 with max_inflight equals 6 and overlaps equals 1492 -- a real data race visible as a wrong number rather than a sanitizer warning -- annotated that the lost count is undefined behaviour and is reported rather than gated, while the overlap is what verify.lua asserts because it is measured with atomics and well defined. The lower band contrasts two correct mechanisms: std::mutex, which BLOCKS a thread into futex FUTEX_WAIT, at 1022 futex calls for 20,000 increments, against an amber strand, which DEFERS the handler and returns the thread to the pool, at 52 futex calls for the identical 20,000 increments. A closing amber note observes that a strand costs no lock and no blocked thread, and pays instead in ordering. Footnotes record that the futex counts were measured with ch51's method, strace -f -c -e trace=futex, and are gated as a ratio rather than as magnitudes because they move with scheduling, and that everything was measured on the Fedora 44 reference host with Boost 1.90.0 and GCC 16.1.1." + caption="Figure 55.1 — one handler, two executors: a strand serializes without a lock, and the arm without one loses increments for real" %} + +> **Tools used** — `g++` and `cmake` (host; gated by `scripts/check-host.sh` as +> `g++ >= 14` and `cmake >= 3.25`), `ninja` (host; gated), `clang++` (host; +> gated, parity gate G), `strace` (host; gated, counts the futex calls in gate D +> and the socket writes in gate E), `timeout` from coreutils (host; bounds every +> case), `lua` (host; gated, runs `verify.lua`), and `python3` (host; gated, runs +> `scripts/test-all-examples.py`). **System Boost 1.90.0** from Fedora's +> `boost-devel` — Asio itself is header-only, and the link line adds only +> `boost_system` for the error categories. **No Conan, no network, and nothing +> binds a port**: the I/O cases use an AF_UNIX socketpair. No VM, no root; +> `examples/55-boost-asio` is `mode: local`. + +## The first surprise: `run()` returns immediately + +Before any of the interesting machinery, the thing that catches everyone once: + +``` +[host]$ ./demo.sh cpp run work-guard +work-guard: without_work_handlers=0 with_guard_handlers=1 posted=1 +work-guard: returned_immediately=yes guard_kept_it_alive=yes +work-guard: run() returns on no OUTSTANDING WORK, not on completion +``` + +Zero handlers. A freshly constructed `io_context` has nothing outstanding, so +`run()` finds nothing to do and returns — and the async operations you were +about to start never happen, because the loop they needed already exited. + +`run()` returns on *no outstanding work*, not on *you are finished*. The claim +that keeps it alive is an `executor_work_guard`: + +```cpp + asio::io_context guarded; + auto guard = asio::make_work_guard(guarded); + int posted = 0; + asio::post(guarded, [&posted, &guard] { + ++posted; + guard.reset(); // release the claim; run() may now return + }); + const std::size_t ran_with = guarded.run(); +``` + +This is worth naming as a structural difference rather than a gotcha. Chapter +9's hand-written loop was a `while (true)` — its lifetime was the loop's own +control flow, and it ran until you broke out of it. An `io_context`'s lifetime is +instead a *refcount on pending work*, which is the right design for a library +that cannot see your control flow, and the wrong intuition for anyone arriving +from a loop they wrote themselves. + +## The strand + +Here is the chapter's central measurement. Two thousand handlers, eight threads +all calling `run()` on one `io_context`, and a counter that is deliberately a +plain `long` — not atomic, not under any lock: + +```cpp +struct Observed { + std::atomic inflight{0}; + std::atomic max_inflight{0}; + std::atomic overlaps{0}; + long unguarded = 0; // intentionally unsynchronized -- that is the point +}; +``` + +The handler body is a single function used by both arms, so the *only* variable +in the experiment is which executor it was posted to: + +```cpp +// The handler body, shared by both variants so the ONLY difference between +// them is which executor it was posted to. +auto make_handler(Observed& o) { + return [&o] { + const int now = o.inflight.fetch_add(1, std::memory_order_relaxed) + 1; + bump_max(o, now); + if (now > 1) { + o.overlaps.fetch_add(1, std::memory_order_relaxed); + } + // Widen the window so overlap is observable rather than theoretical. + for (volatile int k = 0; k < 200; ++k) { + } + ++o.unguarded; + o.inflight.fetch_sub(1, std::memory_order_relaxed); + }; +} +``` + +Through a strand: + +``` +[host]$ ./demo.sh cpp run strand +strand: posts=2000 counter=2000 expected=2000 correct=yes +strand: max_inflight=1 overlaps=0 serialized=yes +strand: 8 threads were running handlers, and no two ran at once +``` + +Exactly 2000, from an unsynchronized increment performed by eight threads. Not +2000 because the race happened not to bite — `max_inflight=1` says no two +handlers were ever simultaneously inside the body, and `overlaps=0` says the +condition never once held. There was no race to lose. + +Now the same handler, posted straight to the `io_context`: + +``` +[host]$ ./demo.sh cpp run nostrand +nostrand: posts=2000 counter=1862 expected=2000 lost=138 +nostrand: max_inflight=6 overlaps=1492 serialized=no +nostrand: same handler, same threads -- only the executor changed +``` + +One hundred and thirty-eight increments gone. Not a warning from a sanitizer, not +a theoretical hazard in a comment — a **wrong number**, produced by the same +function that produced the right one a moment ago. + +That is the whole idea of a strand. It is an executor with one guarantee: handlers +submitted through it never run concurrently, and never overlap, no matter how many +threads are pulling from the underlying context. Serialization without a lock. + +### What the gate asserts, and what it refuses to + +Notice which number `verify.lua` is willing to stand behind: + +```lua +checks.expect_match(tostring(ns_overlaps ~= nil and ns_overlaps > 0), "true", + "cpp: the SAME handler posted straight to the io_context overlaps (" .. + tostring(ns_overlaps) .. " overlapping entries, max_inflight=" .. tostring(ns_inflight) .. + ") -- only the executor changed") +``` + +The overlap, and only the overlap. The lost-increment count is reported by the +program and asserted by nothing, for a reason Chapter 49 made a rule: **an +unsynchronized increment is a data race, a data race is undefined behavior, and +you cannot gate on the output of undefined behavior.** A run that loses nothing +at all is entirely legal, and a test that failed on it would be wrong, not the +run. + +The magnitudes bear that out. Figure 55.1 records the run quoted above, at 1862 +of 2000; five further runs on this host lost 22, 110, 104, 44, and 24 increments +instead. Every one of those is the same defect and none of them is a number to +quote as *the* answer. Over those same runs `overlaps` was 1375 to 1507 and +`max_inflight` 5 to 6, never remotely near zero, which is why the overlap is the +assertion: it is measured with atomics, it is well defined, and it is a real +statement about scheduling rather than about UB. + +## A strand is not a mutex + +The obvious objection is that a mutex would also have produced 2000. It would. +Both arms of the next case are correct: + +``` +[host]$ ./demo.sh cpp run strand-cost +strand-cost: posts=20000 counter=20000 correct=yes + +[host]$ ./demo.sh cpp run mutex-cost +mutex-cost: posts=20000 counter=20000 correct=yes +``` + +The difference is not correctness, it is what each one does to a thread that +loses the race, and Chapter 51 already built the instrument to see it: + +``` +[host]$ strace -f -c -e trace=futex cpp/build/release/asiodemo strand-cost +[host]$ strace -f -c -e trace=futex cpp/build/release/asiodemo mutex-cost +``` + +| mechanism | futex calls | counter | +| --- | --- | --- | +| strand | **52** | 20000 (correct) | +| `std::mutex` | **1022** | 20000 (correct) | + +Roughly twenty times fewer trips into the kernel for byte-identical work. The +reason is structural, and it is the sentence to remember from this chapter: + +**A mutex blocks a thread. A strand defers a handler.** + +Chapter 51 measured exactly what blocking costs — a contended `std::mutex` falls +out of its userspace fast path into `futex(FUTEX_WAIT)`, the thread sleeps, and +someone has to `FUTEX_WAKE` it. That thread is doing nothing in the meantime. A +strand never blocks anybody: if a handler cannot run now because another one from +the same strand is running, the strand queues it and the thread goes back to the +`io_context` for other work. Nothing sleeps, so nothing needs waking. + +The gate takes the sign and refuses the magnitude: + +```lua + checks.expect_match(tostring(n_strand < n_mutex / 2), "true", + "cpp: the strand reached the kernel far less often than the mutex for identical work " .. + "(strand=" .. tostring(n_strand) .. " futex calls, mutex=" .. tostring(n_mutex) .. + ") -- a mutex BLOCKS a thread, a strand DEFERS a handler") +``` + +Three runs each on this host gave 52, 54, 55 against 1022, 992, 965 — the ratio +is rock solid and the individual counts move with scheduling every time, which is +why `strand < mutex/2` is the assertion and `== 52` would be a flake waiting to +happen. + +What the strand pays instead is **ordering**. `asio::post` through a strand does +not run your handler now even when the calling thread is idle and nothing else +holds the strand; it hands the handler to the queue. If you need the work to +happen synchronously when it safely can, that is what `asio::dispatch` is for — +named here, not exercised. + +## Who actually runs the handlers + +{% include excalidraw.html + file="55-topologies-and-gather" + alt="Two Asio facts measured with earlier chapters' instruments. The upper band shows four shapes of run() over the same 400 posted handlers: one thread on one io_context reporting distinct_tids equals 1 and needing no synchronisation at all; an amber N-threads-on-one-context shape, 8 times ioc.run() over a shared queue, reporting distinct_tids equals 8, whose handlers need a strand; one io_context per thread reporting distinct_tids equals 4, where each loop owns its own descriptors; and asio::thread_pool(4) reporting distinct_tids equals 4 with no run() call written anywhere. A note records that the tids come from gettid(), ch50's instrument and ch49's vocabulary. The lower amber band contrasts asio::write given an array of four const_buffers, costing one socket write-family syscall -- a single sendmsg carrying 4 iovecs, 56 bytes, one trip into the kernel -- against four separate asio::write calls costing four sendto, the same 56 bytes in four trips. Footnotes observe that this is the readv and writev machinery Part 5 covered, reached by handing Asio a buffer sequence instead of assembling an iovec array by hand, and that these syscall counts are deterministic and are therefore gated exactly, with plain write(2) excluded because it is this program's own stdout." + caption="Figure 55.2 — the same 400 handlers under four shapes of run(), and four buffers reaching the wire in one syscall instead of four" %} + +A hand-written epoll loop answers "who runs the handler" by construction: the +thread that called `epoll_wait`, because there is only one. Asio makes it a +choice, and the choice is visible from outside the process with the same +instrument Chapter 50 used and the same vocabulary Chapter 49 established: + +``` +[host]$ ./demo.sh cpp run topology-one +topology-one: handlers=400 distinct_tids=1 +[host]$ ./demo.sh cpp run topology-pool +topology-pool: handlers=400 distinct_tids=8 +[host]$ ./demo.sh cpp run topology-percore +topology-percore: handlers=400 distinct_tids=4 +[host]$ ./demo.sh cpp run topology-threadpool +topology-threadpool: handlers=400 distinct_tids=4 +``` + +Four hundred identical handlers every time. Only the shape of the `run()` call +changes, and each shape has a different consequence for the code inside the +handler: + +- **`one`** — one thread, one context. Every handler runs on the caller; + `distinct_tids=1`. Chapter 49's *concurrent, not parallel*, and it needs no + synchronization whatsoever, because there is nothing to synchronize against. +- **`pool`** — eight threads all calling `run()` on one context: a shared queue, + `distinct_tids=8`. This is the shape that makes strands necessary. +- **`percore`** — one `io_context` per thread, `distinct_tids=4`. No sharing and + no cross-thread handoff; each loop owns its own descriptors, which buys back + the `one` shape's freedom from synchronization at the cost of not being able to + balance load between the loops. +- **`threadpool`** — `asio::thread_pool(4)` owns its threads, `distinct_tids=4`, + and there is no `run()` call written anywhere in that branch. + +Gate F takes exactly one exact number and three signs: + +```lua +checks.expect_match(out["topology-one"], "distinct_tids=1", + "cpp: one thread calling run() on one io_context ran every handler itself") +``` + +`one` must be exactly 1, because that one is structural — a single thread cannot +report two tids. The other three are gated `> 1`, because how many of eight +threads actually pick up work before the queue drains is a scheduling outcome. +That is also why `pool` reports 8 rather than the 4 you might expect from its +name: it reuses the eight-thread helper the strand cases use. + +## Four buffers, one syscall + +The other thing a library does above the reactor is know the kernel's calling +conventions. Four pieces of a response: + +```cpp + const std::array bufs{asio::buffer(kStatus), asio::buffer(kType), + asio::buffer(kBlank), asio::buffer(kBody)}; + const std::size_t wrote = asio::write(p.a, bufs); +``` + +against the same four written one at a time: + +```cpp + for (const std::string* s : {&kStatus, &kType, &kBlank, &kBody}) { + wrote += asio::write(p.a, asio::buffer(*s)); // four separate calls + } +``` + +Identical bytes arrive at the peer either way — 56 of them, `intact=yes` on both +— but counted by syscall they are not the same program at all: + +| form | socket write-family syscalls | +| --- | --- | +| `asio::write(sock, array)` | **1** (`sendmsg`, 4 iovecs) | +| four separate `asio::write` calls | **4** (`sendto`) | + +An Asio *buffer sequence* is not a convenience wrapper around a loop. It maps +onto the kernel's iovec form, which is Part 5's `readv`/`writev` machinery +reached without assembling an `iovec` array and getting its lengths right by +hand. + +These counts are gated exactly rather than as a ratio, and the difference from +gate D is the point: syscall counts here are deterministic, while futex counts +are a scheduling outcome. Gate what is deterministic; gate the sign of what is +not. + +One detail in that gate is load-bearing: + +```lua + local r = checks.run("strace -f -c -e trace=writev,sendmsg,sendto " .. BIN .. " " .. case .. + " 2>&1 | awk '/writev|sendmsg|sendto/{s+=$4} END{print s+0}'") +``` + +Plain `write(2)` is excluded. This program prints to stdout, and counting its own +`printf` traffic would swamp a comparison between 1 and 4. The gate also sums +three syscall names rather than matching one, because whether a given socket type +ends up in `sendmsg` or `writev` is a kernel implementation detail — the *count* +is the claim, not the spelling. + +## Cancellation, the third shape + +Part 14 has now measured three different answers to "stop that": + +| chapter | mechanism | what arrives | +| --- | --- | --- | +| 51 | `std::stop_token` | a **flag** the target polls | +| 52 | `boost::thread::interrupt()` | an ordinary **exception**, at defined points | +| 55 | `asio::cancellation_signal` | a **completion** carrying an error code | + +``` +[host]$ ./demo.sh cpp run cancel +cancel: handlers_run=1 handler_ran=yes error=Operation canceled aborted=yes +``` + +A timer with an hour left on it completed immediately. The operation did not +vanish and the handler was not dropped — it ran, exactly once, with +`ec == asio::error::operation_aborted`. + +That is a genuinely different shape from the other two, and its practical +consequence is about ownership rather than about control flow: because a +cancelled operation still completes exactly once, everything the handler owns — +the buffer it was going to fill, the `shared_ptr` keeping the connection alive — +is released on the same path whether the operation succeeded or was cancelled. +Chapter 52's exception model has to unwind to achieve that; Chapter 51's flag +model leaves it entirely to you. + +## How the code works + +Every measurement is its own subcommand, which by Part 14 is a habit with a +reason: `nostrand` deliberately contains a data race, and a process containing +one must not be able to influence the cases that do not. + +The socketpair is what keeps the I/O cases offline: + +```cpp +struct Pair { + asio::io_context ioc; + asio::local::stream_protocol::socket a{ioc}; + asio::local::stream_protocol::socket b{ioc}; + Pair() { asio::local::connect_pair(a, b); } +}; +``` + +`local::connect_pair` is `socketpair(2)` in AF_UNIX with Asio's socket type +wrapped around both ends. Nothing binds, nothing listens, no port is claimed, and +the example runs identically on a machine with no network at all. + +The build carries one deliberate definition: + +```cmake +target_compile_definitions(asiodemo PRIVATE BOOST_ASIO_NO_DEPRECATED) +``` + +which removes `io_service`, `deadline_timer`, and the rest of the pre-1.66 +spellings from the translation unit entirely. They still work, they are still all +over the internet, and a chapter that accidentally demonstrated one would be +teaching an interface Asio has replaced. + +## Errors, three ways + +**An error the type system catches.** `asio::write` on a buffer sequence is the +same function as `asio::write` on a single buffer — the sequence concept is a +compile-time property, so handing it four buffers instead of one is not a +different call to get wrong. There is no length array to desynchronize from the +pointer array, which is the classic `writev` bug Part 5 had to be careful about. + +**An error delivered as a value.** `operation_aborted` arrives in an +`error_code`, in the completion handler, on the normal path. So does every other +Asio error: the overload of `drain` used here passes an `error_code` in and reads +it back, because reading to EOF on a closed socket *reports* an error that is not +one: + +```cpp +std::size_t drain(asio::local::stream_protocol::socket& s) { + std::vector in(4096); + boost::system::error_code ec; + return asio::read(s, asio::buffer(in), ec); +} +``` + +The throwing overload would have raised `eof` there. Choosing the `error_code` +overload is the same decision Chapter 22 made with `EAGAIN` — some conditions the +kernel reports as errors are simply how the operation ends. + +**An error nothing catches.** Forgetting the strand. `nostrand` compiles cleanly, +runs to completion, exits 0, and produces a plausible number that happens to be +wrong; no exception, no error code, no crash, and on a single-threaded +`io_context` it would even be correct. Nothing in the type system distinguishes +`post(strand, h)` from `post(ioc, h)` — both are valid executors — so this is a +defect that only counting can find, which is why this example counts. + +## Concurrency lens + +The rule the whole chapter reduces to: **a handler is not thread-safe merely +because Asio gave it to you.** Under `topology-one` you can touch anything you +like without a lock. Under `topology-pool` the identical handler is a shared +mutable-state problem, and the number of threads calling `run()` is the only +thing that changed. + +What a strand gives you is worth stating precisely, because it is narrower than +"a lock". It guarantees that handlers *on that strand* do not run concurrently, +and that they do not overlap. It guarantees nothing about handlers on a different +strand, or about a thread that touches the same data outside a handler entirely. +The strand is a serialization domain, not a mutual-exclusion primitive attached +to your data — which is why Asio expects you to associate a strand with an +*object* (a connection, a session) and then post everything touching that object +through it. + +Set against the models of the preceding chapters: + +- Chapter 50's pthreads and Chapter 51's `std::thread` synchronize by **blocking a + thread**. Chapter 51 measured the cost as futex traffic; this chapter reuses + that instrument to show the strand avoiding it. +- Chapter 54's fibers under `round_robin` get their safety from **never + migrating** — a fiber's data is touched by one thread by construction. Asio's + `topology-percore` is the same bargain at the level of an event loop, and + `topology-pool` is the trade in the other direction. +- Chapter 53's coroutines suspend only at `co_await` and only in a coroutine body. + Asio's completion handlers have the same property from a different direction: a + handler runs to completion, so anything it does between entry and return is + atomic with respect to other handlers *on the same strand*. + +And the ordering caveat is real. A strand serializes but does not order handlers +against anything outside itself, and `post` is explicitly "later, not now". Code +that assumed a handler had already run because the call to `post` returned is +broken in the same way as code that assumed `run()` would keep going with nothing +outstanding. + +## Build, run, observe + +``` +[host]$ cd examples/55-boost-asio +[host]$ ./demo.sh cpp build +[host]$ for c in versions work-guard strand nostrand strand-cost mutex-cost \ + gather separate cancel topology-one topology-pool \ + topology-percore topology-threadpool; do + ./demo.sh cpp run "$c"; done +``` + +The two cases whose claim is a syscall count are worth running under `strace` +yourself, since that is where the evidence actually lives: + +``` +[host]$ strace -f -c -e trace=futex cpp/build/release/asiodemo strand-cost +[host]$ strace -f -c -e trace=futex cpp/build/release/asiodemo mutex-cost +[host]$ strace -f -c -e trace=writev,sendmsg,sendto cpp/build/release/asiodemo gather +[host]$ strace -f -c -e trace=writev,sendmsg,sendto cpp/build/release/asiodemo separate +``` + +Then the harness: + +``` +[host]$ LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua +``` + +``` +ok: cpp: a fresh io_context::run() returns after ZERO handlers -- it returns on no outstanding work, not on completion +ok: cpp: through a strand, an UNSYNCHRONIZED counter incremented by 8 threads is exactly right (counter=2000) +ok: cpp: never more than one strand handler in flight, across 8 threads calling run() +ok: cpp: and zero overlapping entries -- serialization without a mutex +ok: cpp: the SAME handler posted straight to the io_context overlaps (1394 overlapping entries, max_inflight=5) -- only the executor changed +info: the unguarded counter's lost-update count is NOT gated -- an unsynchronized increment is a data race and therefore UB, so a run that loses nothing is legal. The gate asserts OVERLAP, which is measured with atomics and well defined. +ok: cpp: the strand reached the kernel far less often than the mutex for identical work (strand=52 futex calls, mutex=1022) -- a mutex BLOCKS a thread, a strand DEFERS a handler +ok: cpp: four buffers written as ONE sequence cost exactly one socket write-family syscall (got 1) -- the kernel's iovec form, reached without assembling one by hand +ok: cpp: the identical four buffers written separately cost four (got 4) +ok: cpp: one thread calling run() on one io_context ran every handler itself +ok: cpp: topology-pool spread handlers across 8 OS threads -- same handlers, different shape of run() +ok: cpp: a cancelled operation completes with operation_aborted -- the handler still runs exactly once, so ownership rules never change +PASS 48 / FAIL 0 +``` + +## Cross-check: the same sixteen bytes, still + +All thirteen cases reported `digest=0x481984990deee5ff`, and the clang build +agrees — the same value Chapters 46 through 54 have been carrying, now across two +compilers and seven concurrency models. + +The sharper cross-check is that this chapter borrowed every one of its +instruments rather than inventing any. The futex counter is Chapter 51's, applied +to a mechanism Chapter 51 did not have. `gettid()` is Chapter 50's, applied to an +I/O runtime rather than to threads you created yourself. The iovec claim lands on +Part 5's `writev`. The rule about never asserting on the output of undefined +behavior is Chapter 49's, and it is the reason the most dramatic number in the +chapter — 138 lost increments — appears nowhere in a gate. + +That is also the answer to what a library adds. Not readiness: Chapter 9 has +readiness. What Asio adds is a name for the thing a single-threaded loop never +needed to name, and once you can name it, you can count it. + +## What you learned + +- **`io_context::run()` returns when there is no outstanding work**, not when you + are finished. A fresh context runs zero handlers and returns immediately; + `executor_work_guard` is the claim that keeps it alive. +- **A strand is an executor that serializes handlers without a lock.** The same + handler on 8 threads left a deliberately unsynchronized counter at exactly 2000 + with `max_inflight=1` and `overlaps=0`. +- **Without one, the same code loses increments for real** — 138 of 2000 on the + quoted run, a data race visible as a wrong number rather than as a warning. +- **Never gate on the output of undefined behavior.** The lost count varied 22 to + 138 across runs and is only reported; the overlap is measured with atomics, is + well defined, and is what the gate asserts. +- **A mutex blocks a thread; a strand defers a handler.** Both produced the + correct 20000, at 52 futex calls against 1022 — Chapter 51's instrument pointed + at Chapter 51's alternative. +- **What a strand costs is ordering, not throughput.** `post` means later even + when now would have been safe; `dispatch` is the other choice. +- **A buffer sequence is the kernel's iovec form.** Four buffers as one sequence + cost one `sendmsg` carrying 4 iovecs; the same four written separately cost + four `sendto`, for identical bytes. +- **Gate what is deterministic exactly, and gate the sign of what is not.** + Syscall counts are exact gates here; futex counts are a ratio. +- **Who runs your handler is your choice, and it is visible from outside.** The + same 400 handlers reported 1, 8, 4, and 4 distinct tids under four shapes of + `run()` — and a handler is not thread-safe merely because Asio handed it to you. +- **Asio's cancellation is a completion, not an exception or a flag.** A cancelled + operation still completes exactly once, with `operation_aborted`, so ownership + rules are identical on both paths. + +--- + +

verified — on the Fedora 44 +reference host this session (kernel 7.1.8-200.fc44, glibc 2.43, Boost +1.90.0 from boost-devel-1.90.0-7.fc44, 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 Conan, no network, and nothing +bound to a port, no VM): LSP_LANG=cpp REPO_ROOT=$(cd ../.. +&& pwd) lua verify.lua reported PASS 48 / FAIL 0 with +gates C (the strand), D (futex counts under strace), E +(scatter-gather syscall counts under strace), F (topologies) and G +(clang parity) all running for real, and python3 +scripts/test-all-examples.py --only 55-boost-asio reported 1 passed, +0 failed, 0 skipped. Every transcript quoted above is a real run from this +session. Confirmed live: all thirteen cases produced +digest=0x481984990deee5ff, byte-identical to Chapters 46 through 54 +and matched by the clang build; work-guard reported +without_work_handlers=0 with_guard_handlers=1; strand +reported counter=2000 max_inflight=1 overlaps=0 and +nostrand reported counter=1862 lost=138 max_inflight=6 +overlaps=1492 for the identical handler, with five further runs losing 22, +110, 104, 44 and 24 (overlaps 1375-1507) — the variation is why the lost count is +reported and never gated. Figure 55.1 was regenerated from this session's numbers +and records that same run (1862 of 2000, 1492 overlaps, 52 against 1022 futex +calls); strand-cost and mutex-cost both +reported counter=20000 correct=yes at 52 versus 1022 futex calls in +the gated run, with three further pairs at 52/54/55 against 1022/992/965; +gather cost exactly 1 socket write-family syscall +(sendmsg) and separate exactly 4 +(sendto), both moving 56 bytes with intact=yes; +topology-one|pool|percore|threadpool reported +distinct_tids= 1, 8, 4, +4; cancel reported handlers_run=1 with +error=Operation canceled aborted=yes. Not exercised: unverified — no timing of any kind is +measured or gated in this example (Chapter 39); every claim here is a handler +count, a syscall count, a tid count, or a value that is either correct or not. +asio::dispatch, asio::co_spawn with +use_awaitable, asio::awaitable, and Asio's +ssl and resolver facilities are named or alluded to in the chapter +but are not built, run, or gated. The claim that Asio's Linux backend uses +epoll_wait is Asio's documented implementation rather than something +this example strace's for, since the reactor itself is Chapter 9's subject and is +deliberately not re-derived here. Nothing in this example binds a port or +performs any network I/O: both I/O cases run over an AF_UNIX socketpair from +local::connect_pair.

diff --git a/_plans/r24-ch55-plan.md b/_plans/r24-ch55-plan.md new file mode 100644 index 0000000..bf8863c --- /dev/null +++ b/_plans/r24-ch55-plan.md @@ -0,0 +1,222 @@ +--- +title: "r24 / ch55 — Boost.Asio — plan (internal)" +published: false +--- + +# r24 ch55 — example `55-boost-asio` + chapter + diagrams + +Seventh chapter of Part 14. Scope was settled in `_plans/r18-ch49-plan.md` ("ch55 scope, +settled 2026-08-16") when the outline row was found to list six topics for eight chapters +and ch55 duplicated ch54's description. + +## Overlap check (done FIRST, as in r22 and r23) + +- **ch09** owns epoll readiness, level/edge, `EPOLLONESHOT`, timerfd/signalfd. +- **ch22** owns nonblocking sockets, `EAGAIN`, and the hand-written state machine. +- **ch27** owns a hand-rolled coroutine reactor *over* epoll. +- `grep -i "boost::asio" _docs/*.md` → **only forward-references** (ch49, ch53). No prior + chapter uses Asio. + +**So ch55 must not re-derive epoll, readiness, or `EAGAIN`.** Those three chapters built +the reactor by hand; ch55 is about what a *library* adds on top of it, and every section +below is an Asio concept with no hand-rolled counterpart earlier in the book. + +## Host audit (2026-08-20, Fedora 44, Boost 1.90.0, GCC 16.1.1) + +Header-only Asio + `-lboost_system -pthread`. **No Conan, no network** (the demos use a +`local::connect_pair` socketpair, so nothing binds a port). + +### 1. `executor_work_guard` — why `run()` returns immediately + +| case | `run()` returned after | +| --- | --- | +| no work posted | **0 handlers** | +| work guard held, one post, guard reset | **1 handler** | + +`io_context::run()` returns when there is no *outstanding work*, not when you are done. +The single most common Asio surprise, and it is a two-line measurement. + +### 2. The strand — THE finding + +2000 posts, 8 threads running one `io_context`, a **deliberately non-atomic** `long` +counter incremented in every handler: + +| variant | counter | max_inflight | overlaps | +| --- | --- | --- | --- | +| `post(strand, …)` | **2000** (correct) | **1** | **0** | +| `post(io_context, …)` | **1983** — 17 increments LOST | 6 | 1315 | + +Same code, same threads; the only difference is which executor the handler was posted to. +The unguarded version exhibits a **real, observable data race** — a wrong number, not a +sanitizer warning. This is the book's preferred kind of demonstration. + +### 3. A strand is not a mutex, and it is measurably cheaper + +Same 20000 increments across 8 threads, counted with ch51's method +(`strace -f -c -e trace=futex`): + +| mechanism | futex calls | result | +| --- | --- | --- | +| strand | **53** | 20000 (correct) | +| `std::mutex` | **1106** | 20000 (correct) | + +~20x fewer. Both correct. The reason is structural: a mutex *blocks a thread* on +contention (ch51 measured that as `FUTEX_WAIT`), while a strand *defers the handler* and +lets the thread pick up other work. Serialization without blocking. + +### 4. Scatter-gather is one syscall + +Four buffers written to a socket, counted by syscall: + +| form | write-family syscalls | +| --- | --- | +| `asio::write(sock, array)` | **1 `sendmsg`** | +| four separate `asio::write` calls | **4 `sendto`** | + +Asio maps a buffer *sequence* onto the kernel's iovec form. 4 → 1, and it ties straight +back to Part 5's I/O chapters. + +### 5. Threading topologies, by tid + +400 handlers, tids collected with `gettid()` (ch50's instrument, ch49's vocabulary): + +| topology | distinct tids | +| --- | --- | +| one thread, one `io_context` | **1** | +| N threads on one `io_context` | **4** | +| one `io_context` per thread | **4** | +| `asio::thread_pool(4)` | **4** | + +## The core demo — `asiodemo` (C++-only, `langs: [cpp]`) + +Subcommands: `work-guard`, `strand`, `nostrand`, `strand-cost`, `mutex-cost`, `gather`, +`separate`, `topology-`, `cancel`, `versions`. + +Digest continuity: `digest=0x481984990deee5ff`. + +## Gate tiers + +**Hard (A–F):** +- A. digest identical across every subcommand. +- B. work guard: `run()` returns after 0 handlers with no work; > 0 with a guard. +- C. **the strand**: `strand` reports the exact expected count with `max_inflight=1` and + `overlaps=0`. `nostrand` reports `overlaps > 0` — gate the *overlap*, NOT the lost + count. A lost increment is a data race and therefore UB; its magnitude is not + reproducible and 2000 is a legitimate outcome. (ch49's rule about never gating on UB + output.) Report the count, assert only that handlers overlapped. +- D. strand vs mutex: both produce the correct count, and the strand issues materially + fewer futex calls. Gate the *sign* (strand < mutex/2), never the magnitudes. Degrade + to SKIP if strace cannot attach, as ch51 does. +- E. gather: the 4-buffer write costs exactly one write-family syscall and the separate + form costs four. Gate the counts — they are deterministic, unlike timings. +- F. topologies: `one` reports exactly 1 tid; the three multi-threaded forms report > 1. + +**Gated-if-present (G):** clang parity. + +**Not gated:** anything needing a network port; timings; the exact lost-increment count. + +## Risks + +- **`nostrand`'s data race is UB.** It may legitimately produce 2000 on some run. Gate + `overlaps > 0` (which is a real, non-UB observation about scheduling) and merely + *report* the counter. Never assert it is wrong. +- **strace may be ptrace-restricted** → gate D degrades to an informational SKIP. +- **Syscall choice may vary** (`sendmsg` vs `writev` depending on socket type). Gate the + COUNT of write-family syscalls, not the specific name. +- **No ports.** Use `local::connect_pair` so nothing binds and the example stays + `mode: local` and offline. +- **Do not re-derive ch09/ch22/ch27.** No epoll walkthrough, no `EAGAIN` state machine, + no hand-rolled reactor. Cite them; build on them. + +## Steps + +S1 mint + strip · S2 `cpp/src/asiodemo.cpp` + CMakeLists · S3 `verify.lua` · S4 manifest · +S5 transcripts · S6 `_docs/55-boost-asio.md` · S7 2 diagrams + catalogue · S8 gate matrix · +S9 PR, stop there. + +## Acceptance + +`verify.lua` PASS/FAIL 0 with C, D, E, F running for real; runner 1 passed; `validate.py` +OK; every chapter code block verbatim; footer states Boost 1.90.0, the strand/mutex futex +counts, the syscall counts, and the tid counts as measured. + +## CHECKPOINT (2026-08-20) — example green, chapter NOT yet written + +**Power-loss checkpoint. Everything below is committed and pushed.** + +### State: DONE and green +- `examples/55-boost-asio/` complete: `cpp/src/asiodemo.cpp` (13 subcommands), CMakeLists, + both demo.sh, `verify.lua`. +- `LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua` → **PASS 48 / FAIL 0**, every + gate running for real including strace (D, E) and clang parity (G). +- `python3 scripts/test-all-examples.py --only 55-boost-asio` → 1 passed. +- Registered in `examples/manifest.yaml` (`langs: [cpp]`, `mode: local`, `timeout: 900`). +- Both diagrams emitted AND catalogued: `55-strand-not-a-mutex`, + `55-topologies-and-gather`. +- `validate.py` → OK. + +### REMAINING WORK — DONE (2026-08-21) +1. ~~`examples/55-boost-asio/README.md`~~ rewritten. +2. ~~`_docs/55-boost-asio.md`~~ written — 32 KB, `part: "Compendium: C++ Concurrency"`, + full spine, both `Figure 55.x` includes, Tools used box. +3. Gate matrix, all re-run on the host this session: + - `LSP_LANG=cpp REPO_ROOT=… lua verify.lua` → **PASS 48 / FAIL 0** (C, D, E, F, G real) + - `python3 scripts/test-all-examples.py --only 55-boost-asio` → **1 passed** + - `python3 scripts/validate.py` → **OK** + - verbatim-block check → 12/12 blocks match `asiodemo.cpp` / `verify.lua` / `CMakeLists.txt` + - banned words → none; cross-refs ch09/22/27/39/46/49/50/51/52/53/54 all resolve +4. PR — remaining. + +### Numbers as re-measured 2026-08-21 (what the chapter quotes) +`nostrand` counter=1862 lost=138 max_inflight=6 overlaps=1492; five further runs lost +22, 110, 104, 44, 24 with overlaps 1375-1507. Futex: strand 52 vs mutex 1022 in the gated +run, plus 52/54/55 against 1022/992/965. **Figure 55.1 was regenerated 2026-08-22** to +this session's numbers (was 1897/1441 and 982/61 from the checkpoint session). No spec +script for these figures is committed, so the paired `.svg` + `.excalidraw` were edited in +place: the SVG text is `text-anchor="middle"` on a fixed `x`, and the excalidraw text +elements carry a `width` of exactly `len(text) * 6.05` at `fontSize` 11, which is what a +re-render must reproduce. Dump the excalidraw with `indent=1` to match +`generate_diagram.py` and keep the diff small. Prose carrying those four numbers lives in +four places: the chapter's `alt=` text, the "What the gate asserts" paragraph, the status +footer, and the `assets/diagrams/README.md` catalogue row. +Deterministic and unchanged: gather=1 `sendmsg`, separate=4 `sendto`, 56 bytes; tids +1 / 8 / 4 / 4; digest `0x481984990deee5ff` on all 13 cases and under clang. + +### Captured transcripts (all real runs this session — quote these) +``` +versions: Boost 1.90.0 asio_header_only=yes +work-guard: without_work_handlers=0 with_guard_handlers=1 posted=1 +work-guard: returned_immediately=yes guard_kept_it_alive=yes +strand: posts=2000 counter=2000 expected=2000 correct=yes +strand: max_inflight=1 overlaps=0 serialized=yes +nostrand: posts=2000 counter=1897 expected=2000 lost=103 +nostrand: max_inflight=6 overlaps=1441 serialized=no +gather: buffers=4 wrote=56 peer_read=56 intact=yes +separate: buffers=4 wrote=56 peer_read=56 intact=yes +cancel: handlers_run=1 handler_ran=yes error=Operation canceled aborted=yes +topology-one: handlers=400 distinct_tids=1 +topology-pool: handlers=400 distinct_tids=8 +topology-percore: handlers=400 distinct_tids=4 +topology-threadpool: handlers=400 distinct_tids=4 +``` + +### Stability data (measured, use for the "gate the sign" prose) +`nostrand` over 5 runs: lost = 67, 37, 41, 35, 34 — **magnitude varies every run**, which +is exactly why the lost count is reported and never gated. `overlaps` was 1308–1392 on +those same runs, always >> 0, which is what IS gated. + +futex counts over 3 runs each: `strand-cost` 69 / 50 / 51 · `mutex-cost` 964 / 871 / 957. +Sign rock solid (~15-20x), magnitude never. Gate is `strand < mutex/2`. + +Scatter-gather syscalls (deterministic, gated exactly): `gather` → **1** (`sendmsg` with +4 iovecs); `separate` → **4** (`sendto`). Plain `write(2)` excluded — that is stdout. + +### Notes for the chapter +- ch09/ch22/ch27 own epoll, EAGAIN, and the hand-rolled reactor. **Do not re-derive.** + Cite them; ch55 is about what the library adds above that layer. +- `topology-pool` reports **8**, not 4 — it reuses `run_on_threads` (kHandlerThreads=8). + Gate is `> 1`, so this is fine; just quote 8 accurately. +- The three cancellation models now measured across the book: ch51 `stop_token` (a flag), + ch52 `thread_interrupted` (an ordinary exception), ch55 `cancellation_signal` (a + COMPLETION carrying `operation_aborted` — the handler still runs exactly once, so + ownership rules never change). That is a genuine third shape, worth a section. diff --git a/assets/diagrams/55-strand-not-a-mutex.excalidraw b/assets/diagrams/55-strand-not-a-mutex.excalidraw new file mode 100644 index 0000000..5f1ea66 --- /dev/null +++ b/assets/diagrams/55-strand-not-a-mutex.excalidraw @@ -0,0 +1,1127 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1577504627", + "type": "rectangle", + "x": 24, + "y": 54, + "width": 1112, + "height": 250, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fdf7ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 255406393, + "versionNonce": 103958178, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t399685496", + "type": "text", + "x": 36, + "y": 60, + "width": 481.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1876456987, + "versionNonce": 1129657086, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Same handler, same 8 threads, same io_context \u2014 only the executor differs", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Same handler, same 8 threads, same io_context \u2014 only the executor differs", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r984687756", + "type": "rectangle", + "x": 24, + "y": 322, + "width": 1112, + "height": 190, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1879984719, + "versionNonce": 597403579, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1039875941", + "type": "text", + "x": 36, + "y": 328, + "width": 343.20000000000005, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1011586112, + "versionNonce": 596162066, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "And a strand is not a mutex wearing a different name", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "And a strand is not a mutex wearing a different name", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r601467700", + "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": 1482428824, + "versionNonce": 1453676033, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t976312430", + "type": "text", + "x": 410, + "y": 22, + "width": 292.6, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1999275449, + "versionNonce": 1355817353, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "post(strand, h) vs post(io_context, h)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "post(strand, h) vs post(io_context, h)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r1733276670", + "type": "rectangle", + "x": 56, + "y": 96, + "width": 500, + "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": 1113160835, + "versionNonce": 1101380859, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1101075453", + "type": "text", + "x": 66, + "y": 104, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1019093359, + "versionNonce": 1191066001, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "post(strand, handler)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "post(strand, handler)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t807976253", + "type": "text", + "x": 66, + "y": 124, + "width": 278.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": 1997305162, + "versionNonce": 99318545, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "unsynchronized counter \u2192 2000 of 2000, correct", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "unsynchronized counter \u2192 2000 of 2000, correct", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t510717683", + "type": "text", + "x": 66, + "y": 139, + "width": 187.55, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1268852700, + "versionNonce": 379376520, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "max_inflight = 1 \u00b7 overlaps = 0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "max_inflight = 1 \u00b7 overlaps = 0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1765894114", + "type": "text", + "x": 66, + "y": 154, + "width": 254.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": 1470150635, + "versionNonce": 356579339, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "8 threads ran handlers; no two ran at once", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "8 threads ran handlers; no two ran at once", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1052535868", + "type": "rectangle", + "x": 596, + "y": 96, + "width": 510, + "height": 126, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1014359866, + "versionNonce": 719054001, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1616254462", + "type": "text", + "x": 606, + "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": 512540530, + "versionNonce": 1401736921, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "post(io_context, handler)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "post(io_context, handler)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1209085894", + "type": "text", + "x": 606, + "y": 124, + "width": 187.55, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 348587107, + "versionNonce": 747568094, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the same counter \u2192 1862 of 2000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the same counter \u2192 1862 of 2000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t337328888", + "type": "text", + "x": 606, + "y": 139, + "width": 205.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 482357438, + "versionNonce": 1940216128, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "max_inflight = 6 \u00b7 overlaps = 1492", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "max_inflight = 6 \u00b7 overlaps = 1492", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1859111196", + "type": "text", + "x": 606, + "y": 154, + "width": 260.15000000000003, + "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": 1349412412, + "versionNonce": 1538118783, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "a real data race, visible as a wrong number", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "a real data race, visible as a wrong number", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r698137835", + "type": "rectangle", + "x": 246, + "y": 240, + "width": 670, + "height": 48, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 391693680, + "versionNonce": 599795150, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t362742257", + "type": "text", + "x": 256, + "y": 248, + "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": 1138828232, + "versionNonce": 292165185, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the lost count is UB: reported, never gated", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the lost count is UB: reported, never gated", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1061964850", + "type": "text", + "x": 256, + "y": 268, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 503202004, + "versionNonce": 1753727012, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the OVERLAP is what verify.lua asserts", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the OVERLAP is what verify.lua asserts", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1526050550", + "type": "rectangle", + "x": 56, + "y": 362, + "width": 500, + "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": 646991517, + "versionNonce": 1712220904, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1745515743", + "type": "text", + "x": 66, + "y": 370, + "width": 231.00000000000003, + "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": 1004494673, + "versionNonce": 673085194, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "std::mutex \u2014 BLOCKS the thread", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "std::mutex \u2014 BLOCKS the thread", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t698085418", + "type": "text", + "x": 66, + "y": 390, + "width": 278.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": 1607214846, + "versionNonce": 1528135934, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "contention \u2192 futex(FUTEX_WAIT) into the kernel", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "contention \u2192 futex(FUTEX_WAIT) into the kernel", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t366407491", + "type": "text", + "x": 66, + "y": 405, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1876142171, + "versionNonce": 1795470212, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "1022 futex calls for 20,000 increments", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1022 futex calls for 20,000 increments", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1832309551", + "type": "rectangle", + "x": 596, + "y": 362, + "width": 510, + "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": 1579110157, + "versionNonce": 934286459, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t542883160", + "type": "text", + "x": 606, + "y": 370, + "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": 1411158807, + "versionNonce": 368831810, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "strand \u2014 DEFERS the handler", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "strand \u2014 DEFERS the handler", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1260494192", + "type": "text", + "x": 606, + "y": 390, + "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": 1302755471, + "versionNonce": 1512469836, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the thread returns to the pool and takes other work", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the thread returns to the pool and takes other work", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1869855775", + "type": "text", + "x": 606, + "y": 405, + "width": 235.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": 848126880, + "versionNonce": 1178344153, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "52 futex calls for the identical 20,000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "52 futex calls for the identical 20,000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a834691552", + "type": "arrow", + "x": 556, + "y": 159, + "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": 617276657, + "versionNonce": 1497969126, + "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": "a1637619256", + "type": "arrow", + "x": 556, + "y": 417, + "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": 584844499, + "versionNonce": 226204420, + "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": "t1825625670", + "type": "text", + "x": 40, + "y": 530, + "width": 1046.65, + "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": 1440887284, + "versionNonce": 284719590, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Both mechanisms produce the correct answer. The difference is what happens to a thread that loses the race: a mutex puts it to sleep, a strand gives it something else to do.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Both mechanisms produce the correct answer. The difference is what happens to a thread that loses the race: a mutex puts it to sleep, a strand gives it something else to do.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1460252226", + "type": "text", + "x": 40, + "y": 554, + "width": 834.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": 1416745792, + "versionNonce": 695971041, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Futex counts measured with ch51's method (strace -f -c -e trace=futex). Gated as a ratio, never as magnitudes \u2014 they move with scheduling.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Futex counts measured with ch51's method (strace -f -c -e trace=futex). Gated as a ratio, never as magnitudes \u2014 they move with scheduling.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1510891633", + "type": "text", + "x": 40, + "y": 578, + "width": 1022.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": 89626194, + "versionNonce": 1617888104, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "A strand costs no lock and no thread. What it costs instead is ordering: handlers on one strand can never run in parallel with each other, however many threads you have.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "A strand costs no lock and no thread. What it costs instead is ordering: handlers on one strand can never run in parallel with each other, however many threads you have.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1760685816", + "type": "text", + "x": 40, + "y": 602, + "width": 405.35, + "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": 555027164, + "versionNonce": 4379366, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Measured on the Fedora 44 reference host, Boost 1.90.0, GCC 16.1.1.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Measured on the Fedora 44 reference host, Boost 1.90.0, GCC 16.1.1.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/55-strand-not-a-mutex.svg b/assets/diagrams/55-strand-not-a-mutex.svg new file mode 100644 index 0000000..6405156 --- /dev/null +++ b/assets/diagrams/55-strand-not-a-mutex.svg @@ -0,0 +1,36 @@ + + + +Same handler, same 8 threads, same io_context — only the executor differs + +And a strand is not a mutex wearing a different name + +post(strand, h) vs post(io_context, h) + +post(strand, handler) +unsynchronized counter → 2000 of 2000, correct +max_inflight = 1 · overlaps = 0 +8 threads ran handlers; no two ran at once + +post(io_context, handler) +the same counter → 1862 of 2000 +max_inflight = 6 · overlaps = 1492 +a real data race, visible as a wrong number + +the lost count is UB: reported, never gated +the OVERLAP is what verify.lua asserts + +std::mutex — BLOCKS the thread +contention → futex(FUTEX_WAIT) into the kernel +1022 futex calls for 20,000 increments + +strand — DEFERS the handler +the thread returns to the pool and takes other work +52 futex calls for the identical 20,000 + + +Both mechanisms produce the correct answer. The difference is what happens to a thread that loses the race: a mutex puts it to sleep, a strand gives it something else to do. +Futex counts measured with ch51's method (strace -f -c -e trace=futex). Gated as a ratio, never as magnitudes — they move with scheduling. +A strand costs no lock and no thread. What it costs instead is ordering: handlers on one strand can never run in parallel with each other, however many threads you have. +Measured on the Fedora 44 reference host, Boost 1.90.0, GCC 16.1.1. + \ No newline at end of file diff --git a/assets/diagrams/55-topologies-and-gather.excalidraw b/assets/diagrams/55-topologies-and-gather.excalidraw new file mode 100644 index 0000000..311ac98 --- /dev/null +++ b/assets/diagrams/55-topologies-and-gather.excalidraw @@ -0,0 +1,1420 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r564421904", + "type": "rectangle", + "x": 24, + "y": 54, + "width": 1112, + "height": 234, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 495937188, + "versionNonce": 114877724, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1947094932", + "type": "text", + "x": 36, + "y": 60, + "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": 422149610, + "versionNonce": 100955297, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Who runs the handlers \u2014 same 400 posts, four shapes of run()", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Who runs the handlers \u2014 same 400 posts, four shapes of run()", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1946996309", + "type": "rectangle", + "x": 24, + "y": 306, + "width": 1112, + "height": 190, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fdf7ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1820334400, + "versionNonce": 887900476, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1687512000", + "type": "text", + "x": 36, + "y": 312, + "width": 224.4, + "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": 1048837573, + "versionNonce": 335278428, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "One buffer SEQUENCE is one syscall", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "One buffer SEQUENCE is one syscall", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1856200023", + "type": "rectangle", + "x": 410, + "y": 14, + "width": 340, + "height": 30, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 727662629, + "versionNonce": 1725225492, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t662218106", + "type": "text", + "x": 420, + "y": 22, + "width": 215.60000000000002, + "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": 232158428, + "versionNonce": 1036386653, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the shape is yours to choose", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the shape is yours to choose", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r1263999410", + "type": "rectangle", + "x": 48, + "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": 1721921672, + "versionNonce": 1240181602, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t265609020", + "type": "text", + "x": 58, + "y": 104, + "width": 177.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1030933016, + "versionNonce": 1158744999, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one thread, one context", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one thread, one context", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t421681318", + "type": "text", + "x": 58, + "y": 124, + "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": 1549955286, + "versionNonce": 841714450, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ioc.run() on main", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ioc.run() on main", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t360838888", + "type": "text", + "x": 58, + "y": 139, + "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": 250770334, + "versionNonce": 774882845, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "distinct_tids = 1", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "distinct_tids = 1", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t558879520", + "type": "text", + "x": 58, + "y": 154, + "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": 1522075954, + "versionNonce": 1001359885, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no synchronisation needed", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no synchronisation needed", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1201799336", + "type": "rectangle", + "x": 318, + "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": 639096556, + "versionNonce": 1848762191, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t586366931", + "type": "text", + "x": 328, + "y": 104, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 226924853, + "versionNonce": 1743485969, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "N threads, one context", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "N threads, one context", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1500766967", + "type": "text", + "x": 328, + "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": 1166970019, + "versionNonce": 455996113, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "8 x ioc.run(), shared queue", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "8 x ioc.run(), shared queue", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1785871732", + "type": "text", + "x": 328, + "y": 139, + "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": 1928104100, + "versionNonce": 1822467811, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "distinct_tids = 8", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "distinct_tids = 8", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1867770655", + "type": "text", + "x": 328, + "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": 1976974631, + "versionNonce": 1722119459, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "handlers need a strand", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "handlers need a strand", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r973407043", + "type": "rectangle", + "x": 588, + "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": 515279654, + "versionNonce": 1652285645, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1038534875", + "type": "text", + "x": 598, + "y": 104, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1446046114, + "versionNonce": 1575774769, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one context per thread", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one context per thread", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1702379896", + "type": "text", + "x": 598, + "y": 124, + "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": 869987092, + "versionNonce": 836759211, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no sharing, no handoff", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no sharing, no handoff", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1613058136", + "type": "text", + "x": 598, + "y": 139, + "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": 601410063, + "versionNonce": 1086806916, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "distinct_tids = 4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "distinct_tids = 4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1532833649", + "type": "text", + "x": 598, + "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": 1484647042, + "versionNonce": 711975128, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "each loop owns its fds", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "each loop owns its fds", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1057513379", + "type": "rectangle", + "x": 858, + "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": 358839123, + "versionNonce": 1263796065, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t935856143", + "type": "text", + "x": 868, + "y": 104, + "width": 154.0, + "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": 1865612898, + "versionNonce": 1661201562, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "asio::thread_pool(4)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "asio::thread_pool(4)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1819584768", + "type": "text", + "x": 868, + "y": 124, + "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": 1314428919, + "versionNonce": 1335996230, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "owns its own threads", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "owns its own threads", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1999486536", + "type": "text", + "x": 868, + "y": 139, + "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": 1680409699, + "versionNonce": 954388640, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "distinct_tids = 4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "distinct_tids = 4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t48540847", + "type": "text", + "x": 868, + "y": 154, + "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": 758807922, + "versionNonce": 1870831709, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no run() call at all", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no run() call at all", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1059841936", + "type": "rectangle", + "x": 286, + "y": 222, + "width": 590, + "height": 48, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 278922249, + "versionNonce": 522108511, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1714024253", + "type": "text", + "x": 296, + "y": 230, + "width": 377.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 378717481, + "versionNonce": 465783443, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tids from gettid() \u2014 ch50's instrument and ch49's", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tids from gettid() \u2014 ch50's instrument and ch49's", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1263872140", + "type": "text", + "x": 296, + "y": 250, + "width": 223.85000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1089867945, + "versionNonce": 36706526, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "vocabulary, applied to an I/O runtime", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "vocabulary, applied to an I/O runtime", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1103262621", + "type": "rectangle", + "x": 56, + "y": 348, + "width": 500, + "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": 1065472230, + "versionNonce": 1219842153, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t651188327", + "type": "text", + "x": 66, + "y": 356, + "width": 308.0, + "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": 1130787898, + "versionNonce": 1776606300, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "asio::write(sock, array)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "asio::write(sock, array)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1115963272", + "type": "text", + "x": 66, + "y": 376, + "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": 927470268, + "versionNonce": 1817394931, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\u2192 1 socket write-family syscall (sendmsg, 4 iovecs)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\u2192 1 socket write-family syscall (sendmsg, 4 iovecs)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1995876533", + "type": "text", + "x": 66, + "y": 391, + "width": 205.70000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1075329634, + "versionNonce": 1439733250, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "56 bytes, one trip into the kernel", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "56 bytes, one trip into the kernel", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1032288911", + "type": "rectangle", + "x": 596, + "y": 348, + "width": 510, + "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": 129573694, + "versionNonce": 1972176208, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1993249715", + "type": "text", + "x": 606, + "y": 356, + "width": 238.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 993803579, + "versionNonce": 1756352170, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "four separate asio::write calls", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "four separate asio::write calls", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1083406110", + "type": "text", + "x": 606, + "y": 376, + "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": 850813028, + "versionNonce": 85462401, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\u2192 4 socket write-family syscalls (sendto)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\u2192 4 socket write-family syscalls (sendto)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t813990692", + "type": "text", + "x": 606, + "y": 391, + "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": 904543406, + "versionNonce": 1979304622, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the same 56 bytes, four trips", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the same 56 bytes, four trips", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1590506540", + "type": "arrow", + "x": 556, + "y": 403, + "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": 843139269, + "versionNonce": 531811315, + "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": "t684630219", + "type": "text", + "x": 40, + "y": 514, + "width": 859.1, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 133158314, + "versionNonce": 176508137, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Identical bytes reach the peer either way \u2014 the gate checks that too. What differs is the number of times the process crosses into the kernel.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Identical bytes reach the peer either way \u2014 the gate checks that too. What differs is the number of times the process crosses into the kernel.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t848703042", + "type": "text", + "x": 40, + "y": 538, + "width": 834.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": 1451615333, + "versionNonce": 523316946, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "This is the readv/writev machinery Part 5 covered, reached by handing Asio a buffer sequence instead of assembling an iovec array by hand.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "This is the readv/writev machinery Part 5 covered, reached by handing Asio a buffer sequence instead of assembling an iovec array by hand.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1402082365", + "type": "text", + "x": 40, + "y": 562, + "width": 744.1500000000001, + "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": 753922924, + "versionNonce": 1406296034, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Syscall counts are deterministic and are gated exactly; plain write(2) is excluded because it is this program's own stdout.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Syscall counts are deterministic and are gated exactly; plain write(2) is excluded because it is this program's own stdout.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t822407181", + "type": "text", + "x": 40, + "y": 586, + "width": 804.6500000000001, + "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": 1683264315, + "versionNonce": 226669701, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Nothing binds a port: the I/O cases use an AF_UNIX socketpair via local::connect_pair, so the example stays offline and unprivileged.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Nothing binds a port: the I/O cases use an AF_UNIX socketpair via local::connect_pair, so the example stays offline and unprivileged.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/55-topologies-and-gather.svg b/assets/diagrams/55-topologies-and-gather.svg new file mode 100644 index 0000000..e426e9a --- /dev/null +++ b/assets/diagrams/55-topologies-and-gather.svg @@ -0,0 +1,45 @@ + + + +Who runs the handlers — same 400 posts, four shapes of run() + +One buffer SEQUENCE is one syscall + +the shape is yours to choose + +one thread, one context +ioc.run() on main +distinct_tids = 1 +no synchronisation needed + +N threads, one context +8 x ioc.run(), shared queue +distinct_tids = 8 +handlers need a strand + +one context per thread +no sharing, no handoff +distinct_tids = 4 +each loop owns its fds + +asio::thread_pool(4) +owns its own threads +distinct_tids = 4 +no run() call at all + +tids from gettid() — ch50's instrument and ch49's +vocabulary, applied to an I/O runtime + +asio::write(sock, array<const_buffer,4>) +→ 1 socket write-family syscall (sendmsg, 4 iovecs) +56 bytes, one trip into the kernel + +four separate asio::write calls +→ 4 socket write-family syscalls (sendto) +the same 56 bytes, four trips + +Identical bytes reach the peer either way — the gate checks that too. What differs is the number of times the process crosses into the kernel. +This is the readv/writev machinery Part 5 covered, reached by handing Asio a buffer sequence instead of assembling an iovec array by hand. +Syscall counts are deterministic and are gated exactly; plain write(2) is excluded because it is this program's own stdout. +Nothing binds a port: the I/O cases use an AF_UNIX socketpair via local::connect_pair, so the example stays offline and unprivileged. + \ No newline at end of file diff --git a/assets/diagrams/README.md b/assets/diagrams/README.md index 3b705dc..c8f28b8 100644 --- a/assets/diagrams/README.md +++ b/assets/diagrams/README.md @@ -107,3 +107,5 @@ here as you add them: | `53-halo-is-permitted` | 53 | heap allocation elision as a permission rather than a guarantee -- an upper band quoting `[dcl.fct.def.coroutine]` that an implementation MAY elide the frame allocation when it provably does not outlive the caller, beside a note that the test case is the easiest possible one (`suspend_never` at both ends, frame dies inside the call); over an amber band of what the two compilers actually do across 1000 calls, where g++ 16.1.1 elides none at `-O0` through `-O3` and clang++ 22.1.8 elides every one from `-O1` up, closing on the observation that the same source at the same `-O2` yields 1000 heap allocations or zero, so "the optimizer will remove it" is a claim about your compiler and not about C++ | | `54-three-ways-to-pause` | 54 | the memory trilogy Part 14 has been building, all three numbers measured on this host -- a kernel-scheduled band for ch50's thread at 8388608 bytes (preempted anywhere, a tid in `/proc`, a kernel `task_struct`), an amber user-scheduled stackful band for ch54's fiber at 131072 bytes with 64 fitting in one thread stack (suspends from anywhere in an ordinary call tree, measured at 6 frames deep, with no `co_await` and no function colouring), and a user-scheduled stackless band for ch53's coroutine frame at 32 bytes with 4096 fitting in one fiber stack (suspends only at `co_await` in a coroutine body, so every frame in the suspend path must itself be a coroutine); closing on 8 MiB → 128 KiB → 32 bytes, with the observation that cost and capability move together in opposite directions | | `54-scheduler-is-a-choice` | 54 | the same 16 fibers under two schedulers, one line apart -- a left column for the default `algo::round_robin` with one per-thread queue where a fiber never leaves the thread that created it, reporting `distinct_tids = 1`, annotated as ch49's concurrent-but-not-parallel cell and noted as a legitimate choice since no migration means no cross-thread synchronisation; and an amber right column for `algo::shared_work` with one shared queue that 4 threads pull from, reporting `distinct_tids = 4`, reaching ch49's concurrent-and-parallel cell at the price of several threads touching your fiber's data; joined by an amber box holding the single `use_scheduling_algorithm` call and the note that in C++ this is yours to write rather than a runtime's to decide, contrasting ch44's Go | +| `55-strand-not-a-mutex` | 55 | the same handler and the same 8 threads posted two ways -- an amber `post(strand, handler)` box where an unsynchronized counter reads 2000 of 2000 with `max_inflight = 1` and `overlaps = 0`, beside a dashed ghost `post(io_context, handler)` box where the same counter reads 1862 of 2000 with `max_inflight = 6` and `overlaps = 1492`, a real data race visible as a wrong number, noting that the lost count is UB and is reported rather than gated while the overlap is what `verify.lua` asserts; over a lower band contrasting `std::mutex` which BLOCKS a thread into `futex(FUTEX_WAIT)` at 1022 futex calls for 20,000 increments with an amber strand which DEFERS the handler and returns the thread to the pool at 52 calls for identical work, closing on the observation that a strand costs no lock and no thread but pays in ordering | +| `55-topologies-and-gather` | 55 | two Asio facts measured with earlier chapters' instruments -- an upper band of four shapes of `run()` over the same 400 posts (one thread on one context at `distinct_tids = 1` needing no synchronisation, an amber N-threads-one-context at 8 whose handlers need a strand, one context per thread at 4 where each loop owns its fds, and `asio::thread_pool(4)` at 4 with no `run()` call at all), tids from `gettid()` per ch50's instrument and ch49's vocabulary; and a lower amber band showing `asio::write` with a 4-element `const_buffer` array costing one socket write-family syscall (`sendmsg` carrying 4 iovecs) against four separate `asio::write` calls costing four `sendto`, the same 56 bytes either way, reached over an AF_UNIX socketpair so nothing binds a port | diff --git a/examples/55-boost-asio/README.md b/examples/55-boost-asio/README.md new file mode 100644 index 0000000..bb6a446 --- /dev/null +++ b/examples/55-boost-asio/README.md @@ -0,0 +1,160 @@ +# 55 — Boost.Asio: what a library adds on top of a reactor + +Chapter 9 built an epoll loop by hand. Chapter 22 wrote the nonblocking/`EAGAIN` +state machine. Chapter 27 drove a hand-rolled coroutine reactor over epoll. +Those three own the readiness model, and **nothing here repeats it**. + +This example measures the concepts Asio has that a hand-written loop does not: +who runs a handler, whether two can run at once, and what that guarantee costs. + +## `run()` returns when there is no work — not when you are done + +``` +[host]$ ./demo.sh cpp run work-guard +work-guard: without_work_handlers=0 with_guard_handlers=1 posted=1 +work-guard: returned_immediately=yes guard_kept_it_alive=yes +``` + +A freshly constructed `io_context` has no outstanding work, so `run()` returns +after **zero** handlers and every async operation you were about to start never +happens. `executor_work_guard` is the claim that says "more is coming". + +## The strand — the finding + +2000 posts, 8 threads on one `io_context`, a deliberately non-atomic `long` +counter incremented in every handler. The handler body is *the same function* in +both runs; only the executor it was posted to differs. + +``` +[host]$ ./demo.sh cpp run strand +strand: posts=2000 counter=2000 expected=2000 correct=yes +strand: max_inflight=1 overlaps=0 serialized=yes + +[host]$ ./demo.sh cpp run nostrand +nostrand: posts=2000 counter=1862 expected=2000 lost=138 +nostrand: max_inflight=6 overlaps=1492 serialized=no +``` + +Serialization without a mutex, and — on the other arm — a real data race visible +as a **wrong number** rather than a sanitizer warning. + +**The lost count is never gated.** An unsynchronized increment is a data race and +therefore UB, so a run that loses nothing is legal; across five consecutive runs +here it was 22, 110, 104, 44, 24. What is gated is the **overlap**, which is +measured with atomics and is well defined. + +## A strand is not a mutex, and it is measurably cheaper + +The same 20000 increments, counted with Chapter 51's method +(`strace -f -c -e trace=futex`): + +| mechanism | futex calls | result | +| --- | --- | --- | +| strand | **52** | 20000 (correct) | +| `std::mutex` | **1022** | 20000 (correct) | + +Both are correct. The difference is structural: a mutex **blocks a thread** on +contention, which is what Chapter 51 measured as `FUTEX_WAIT`; a strand **defers +the handler** and returns the thread to the pool to pick up other work. + +## Scatter-gather is one syscall + +``` +[host]$ ./demo.sh cpp run gather +gather: buffers=4 wrote=56 peer_read=56 intact=yes + +[host]$ ./demo.sh cpp run separate +separate: buffers=4 wrote=56 peer_read=56 intact=yes +``` + +Identical bytes on the wire, counted by syscall: the 4-element `const_buffer` +array costs **one `sendmsg` carrying 4 iovecs**; four separate `asio::write` +calls cost **four `sendto`**. This is Part 5's `readv`/`writev` machinery, +reached by handing Asio a buffer sequence instead of assembling an iovec array. + +## Who runs the handlers + +400 posts, tids from `gettid()` (Chapter 50's instrument, Chapter 49's +vocabulary): + +| topology | distinct tids | +| --- | --- | +| `topology-one` — one thread, one context | **1** | +| `topology-pool` — 8 threads, one context | **8** | +| `topology-percore` — one context per thread | **4** | +| `topology-threadpool` — `asio::thread_pool(4)` | **4** | + +Same handlers every time. The only variable is the shape of the `run()` call. + +## Cancellation, the third shape + +``` +[host]$ ./demo.sh cpp run cancel +cancel: handlers_run=1 handler_ran=yes error=Operation canceled aborted=yes +``` + +Chapter 51 measured `std::stop_token` (a flag the target polls). Chapter 52 +measured `boost::thread::interrupt()` (an ordinary exception). Asio's is a +**completion carrying `operation_aborted`** — a one-hour timer's handler runs +immediately, exactly once, so the ownership rules never change. + +## Layout + +``` +55-boost-asio/ +├── demo.sh # dispatcher (C++ only) +├── verify.lua # gates A-F hard, G skip-if-present +└── cpp/ + ├── CMakeLists.txt + ├── CMakePresets.json + ├── demo.sh + └── src/asiodemo.cpp +``` + +**System Boost only — no Conan, no network.** Asio is header-only here; the link +line is `Boost::system` (for the error categories) plus `Threads::Threads`. The +translation unit is compiled `-DBOOST_ASIO_NO_DEPRECATED` so nothing accidentally +demonstrates an interface Asio has replaced. + +**Nothing binds a port.** The I/O cases use `asio::local::connect_pair`, an +AF_UNIX socketpair, so the example stays `mode: local`, offline, and +unprivileged. + +## Build and run + +``` +[host]$ ./demo.sh cpp build +[host]$ for c in versions work-guard strand nostrand strand-cost mutex-cost \ + gather separate cancel topology-one topology-pool \ + topology-percore topology-threadpool; do + ./demo.sh cpp run "$c"; done +``` + +## Verification + +``` +[host]$ LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua +``` + +- **A** — every one of the 13 cases computes `digest=0x481984990deee5ff`, + byte-identical to Chapters 46 through 54. +- **B** — a fresh `io_context::run()` returns after zero handlers; with a work + guard held it runs the posted one. +- **C** — through a strand the unsynchronized counter is exactly 2000 with + `max_inflight=1` and `overlaps=0`; posted straight to the context the same + handler overlaps. **The overlap is gated, the lost count is not** — never gate + on the output of undefined behavior (Chapter 49's rule). +- **D** — both arms produce the correct 20000, and the strand issues materially + fewer futex calls. Gated as a **ratio** (`strand < mutex/2`), never as + magnitudes. Degrades to an informational SKIP if `strace` cannot attach. +- **E** — the 4-buffer sequence costs exactly **1** socket write-family syscall + and the separate form exactly **4**. These are deterministic, so they are gated + exactly. Plain `write(2)` is excluded — that is this program's own stdout. +- **F** — `topology-one` reports exactly 1 tid; the three multi-threaded shapes + report more than 1. Sign gated, never magnitude. + +**G** (clang parity on the digest and on `overlaps=0`) runs if `clang++` is +present. + +**Not gated:** timings (Chapter 39 — this book does not gate on durations); the +exact lost-increment count; anything needing a network port. diff --git a/examples/55-boost-asio/cpp/.clang-format b/examples/55-boost-asio/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/55-boost-asio/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/55-boost-asio/cpp/.clang-tidy b/examples/55-boost-asio/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/55-boost-asio/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/55-boost-asio/cpp/CMakeLists.txt b/examples/55-boost-asio/cpp/CMakeLists.txt new file mode 100644 index 0000000..44a2a0c --- /dev/null +++ b/examples/55-boost-asio/cpp/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.25) +project(asiodemo CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +# CMake 4 removed its own FindBoost; select Boost's installed BoostConfig.cmake. +# Guarded so older CMake still configures. (Same as ch52 and ch54.) +if(POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) +endif() +# Asio itself is header-only; boost_system supplies the error_category symbols. +find_package(Boost 1.80 REQUIRED COMPONENTS system) + +add_executable(asiodemo src/asiodemo.cpp) +target_link_libraries(asiodemo PRIVATE Boost::system Threads::Threads) + +# BOOST_ASIO_NO_DEPRECATED keeps the old io_service/deadline_timer spellings out +# of scope, so nothing in this example accidentally demonstrates an interface +# Asio has replaced. +target_compile_definitions(asiodemo PRIVATE BOOST_ASIO_NO_DEPRECATED) diff --git a/examples/55-boost-asio/cpp/CMakePresets.json b/examples/55-boost-asio/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/55-boost-asio/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/55-boost-asio/cpp/demo.sh b/examples/55-boost-asio/cpp/demo.sh new file mode 100755 index 0000000..30f1a2e --- /dev/null +++ b/examples/55-boost-asio/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/asiodemo + +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 versions + ;; + *) + echo "usage: $0 [build|run ]" >&2 + exit 2 + ;; +esac diff --git a/examples/55-boost-asio/cpp/src/asiodemo.cpp b/examples/55-boost-asio/cpp/src/asiodemo.cpp new file mode 100644 index 0000000..4ba669d --- /dev/null +++ b/examples/55-boost-asio/cpp/src/asiodemo.cpp @@ -0,0 +1,440 @@ +// asiodemo -- the reference program for ch55. +// +// ch09 built an epoll loop by hand. ch22 wrote the nonblocking/EAGAIN state +// machine. ch27 drove a hand-rolled coroutine reactor over epoll. All three +// own the readiness model, and NOTHING here repeats it. +// +// This program is about what a library adds on top of that reactor -- the +// concepts Asio has that a hand-written loop does not, each measured: +// +// work-guard why io_context::run() returns immediately, and the fix +// strand serialized handlers WITHOUT a mutex -- and the data race +// nostrand the same code without it, losing increments for real +// strand-cost strand vs mutex, counted in futex calls (ch51's method) +// mutex-cost the comparison arm +// gather 4 buffers -> ONE write-family syscall +// separate the same 4 buffers -> four syscalls +// topology-* who actually runs the handlers, by gettid() +// cancel cancellation_signal vs ch51's std::stop_token +// +// Nothing here binds a port: the I/O demos use a AF_UNIX socketpair via +// local::connect_pair, so the example stays offline and unprivileged. +// +// Nothing here is timed (see ch39). Every observable is a handler count, a +// syscall count, a tid count, or a value that is either correct or not. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace asio = boost::asio; + +namespace { + +// ch46-ch54's payload ("The quick brown."), carried forward unchanged. +constexpr std::uint8_t kPayload[16] = {0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69, 0x63, + 0x6b, 0x20, 0x62, 0x72, 0x6f, 0x77, 0x6e, 0x2e}; + +constexpr std::uint64_t kFnvOffsetBasis = 0xcbf29ce484222325ULL; +constexpr std::uint64_t kFnvPrime = 0x00000100000001b3ULL; + +std::uint64_t fnv1a(const std::uint8_t* data, std::size_t len) { + std::uint64_t h = kFnvOffsetBasis; + for (std::size_t i = 0; i < len; ++i) { + h ^= data[i]; + h *= kFnvPrime; // uint64_t wraps by definition -- no UB to sanitize + } + return h; +} + +void report(const char* what) { + std::printf("asio report: case=%s digest=0x%016llx\n", what, + static_cast(fnv1a(kPayload, sizeof kPayload))); +} + +constexpr int kHandlerThreads = 8; +constexpr int kPosts = 2000; +constexpr int kCostPosts = 20000; + +// ── work guard: why run() returns immediately ──────────────────────────── +// +// The most common first surprise in Asio. io_context::run() returns when the +// context has no OUTSTANDING WORK -- not when you have finished with it. A +// freshly constructed context has none, so run() returns instantly and every +// async operation you were about to start never happens. +// +// executor_work_guard is a claim on the context that says "more work is +// coming, do not return yet". Releasing it lets run() finish. + +void case_work_guard() { + asio::io_context idle; + const std::size_t ran_without = idle.run(); + + asio::io_context guarded; + auto guard = asio::make_work_guard(guarded); + int posted = 0; + asio::post(guarded, [&posted, &guard] { + ++posted; + guard.reset(); // release the claim; run() may now return + }); + const std::size_t ran_with = guarded.run(); + + std::printf("work-guard: without_work_handlers=%zu with_guard_handlers=%zu posted=%d\n", + ran_without, ran_with, posted); + std::printf("work-guard: returned_immediately=%s guard_kept_it_alive=%s\n", + ran_without == 0 ? "yes" : "no", ran_with > 0 ? "yes" : "no"); + std::printf("work-guard: run() returns on no OUTSTANDING WORK, not on completion\n"); + report("work-guard"); +} + +// ── the strand ─────────────────────────────────────────────────────────── +// +// A strand is an executor that guarantees the handlers posted through it never +// run concurrently -- and never overlap even when eight threads are pulling +// from the same io_context. +// +// The counter below is a plain `long`, deliberately NOT atomic and NOT under +// any lock. Through a strand that is correct by construction. Posted straight +// to the io_context it is a data race, and the demo reports what that costs. + +struct Observed { + std::atomic inflight{0}; + std::atomic max_inflight{0}; + std::atomic overlaps{0}; + long unguarded = 0; // intentionally unsynchronized -- that is the point +}; + +void bump_max(Observed& o, int now) { + int seen = o.max_inflight.load(std::memory_order_relaxed); + while (now > seen && !o.max_inflight.compare_exchange_weak(seen, now, std::memory_order_relaxed)) { + // compare_exchange_weak refreshes `seen`; loop until we win or lose + } +} + +void run_on_threads(asio::io_context& ioc) { + std::vector threads; + threads.reserve(kHandlerThreads); + for (int i = 0; i < kHandlerThreads; ++i) { + threads.emplace_back([&ioc] { ioc.run(); }); + } + for (auto& t : threads) { + t.join(); + } +} + +// The handler body, shared by both variants so the ONLY difference between +// them is which executor it was posted to. +auto make_handler(Observed& o) { + return [&o] { + const int now = o.inflight.fetch_add(1, std::memory_order_relaxed) + 1; + bump_max(o, now); + if (now > 1) { + o.overlaps.fetch_add(1, std::memory_order_relaxed); + } + // Widen the window so overlap is observable rather than theoretical. + for (volatile int k = 0; k < 200; ++k) { + } + ++o.unguarded; + o.inflight.fetch_sub(1, std::memory_order_relaxed); + }; +} + +void case_strand() { + Observed o; + asio::io_context ioc; + auto strand = asio::make_strand(ioc); + for (int i = 0; i < kPosts; ++i) { + asio::post(strand, make_handler(o)); + } + run_on_threads(ioc); + + std::printf("strand: posts=%d counter=%ld expected=%d correct=%s\n", kPosts, o.unguarded, kPosts, + o.unguarded == kPosts ? "yes" : "no"); + std::printf("strand: max_inflight=%d overlaps=%d serialized=%s\n", o.max_inflight.load(), + o.overlaps.load(), o.overlaps.load() == 0 ? "yes" : "no"); + std::printf("strand: %d threads were running handlers, and no two ran at once\n", + kHandlerThreads); + report("strand"); +} + +void case_nostrand() { + Observed o; + asio::io_context ioc; + for (int i = 0; i < kPosts; ++i) { + asio::post(ioc, make_handler(o)); // straight to the context, no strand + } + run_on_threads(ioc); + + // The counter is reported but NEVER asserted on: an unsynchronized + // increment is a data race, which is undefined behavior, and the number + // of lost updates is not reproducible. What IS a real observation is that + // handlers overlapped -- that is measured with atomics and is well defined. + std::printf("nostrand: posts=%d counter=%ld expected=%d lost=%ld\n", kPosts, o.unguarded, kPosts, + static_cast(kPosts) - o.unguarded); + std::printf("nostrand: max_inflight=%d overlaps=%d serialized=%s\n", o.max_inflight.load(), + o.overlaps.load(), o.overlaps.load() == 0 ? "yes" : "no"); + std::printf("nostrand: same handler, same threads -- only the executor changed\n"); + report("nostrand"); +} + +// ── a strand is not a mutex ────────────────────────────────────────────── +// +// Both of these produce the correct count. The difference is what they do to +// a thread that loses the race for the shared resource: +// +// mutex BLOCKS the thread -- and ch51 measured what blocking costs: +// futex(FUTEX_WAIT) into the kernel, then FUTEX_WAKE to get out +// strand DEFERS the handler and returns the thread to the pool, where it +// picks up other work. Nothing blocks, so nothing sleeps. +// +// Count them from outside with ch51's method: +// strace -f -c -e trace=futex ./asiodemo strand-cost + +void case_strand_cost() { + long counter = 0; + asio::io_context ioc; + auto strand = asio::make_strand(ioc); + for (int i = 0; i < kCostPosts; ++i) { + asio::post(strand, [&counter] { ++counter; }); + } + run_on_threads(ioc); + std::printf("strand-cost: posts=%d counter=%ld correct=%s\n", kCostPosts, counter, + counter == kCostPosts ? "yes" : "no"); + report("strand-cost"); +} + +void case_mutex_cost() { + long counter = 0; + std::mutex m; + asio::io_context ioc; + for (int i = 0; i < kCostPosts; ++i) { + asio::post(ioc, [&counter, &m] { + const std::lock_guard lock(m); + ++counter; + }); + } + run_on_threads(ioc); + std::printf("mutex-cost: posts=%d counter=%ld correct=%s\n", kCostPosts, counter, + counter == kCostPosts ? "yes" : "no"); + report("mutex-cost"); +} + +// ── scatter-gather ─────────────────────────────────────────────────────── +// +// Asio's buffer sequences are not a convenience wrapper around a loop. A +// sequence maps onto the kernel's iovec form, so four buffers leave in ONE +// syscall -- which is the same readv/writev machinery Part 5 covered, reached +// without assembling an iovec array by hand. +// +// A socketpair rather than a real socket: nothing binds, nothing listens, +// the example stays offline. + +struct Pair { + asio::io_context ioc; + asio::local::stream_protocol::socket a{ioc}; + asio::local::stream_protocol::socket b{ioc}; + Pair() { asio::local::connect_pair(a, b); } +}; + +const std::string kStatus = "HTTP/1.1 200 OK\r\n"; +const std::string kType = "Content-Type: text/plain\r\n"; +const std::string kBlank = "\r\n"; +const std::string kBody = "hello world"; + +std::size_t drain(asio::local::stream_protocol::socket& s) { + std::vector in(4096); + boost::system::error_code ec; + return asio::read(s, asio::buffer(in), ec); +} + +void case_gather() { + Pair p; + const std::array bufs{asio::buffer(kStatus), asio::buffer(kType), + asio::buffer(kBlank), asio::buffer(kBody)}; + const std::size_t wrote = asio::write(p.a, bufs); + p.a.close(); + const std::size_t got = drain(p.b); + + std::printf("gather: buffers=4 wrote=%zu peer_read=%zu intact=%s\n", wrote, got, + wrote == got ? "yes" : "no"); + std::printf("gather: one buffer SEQUENCE -> one write-family syscall carrying 4 iovecs\n"); + report("gather"); +} + +void case_separate() { + Pair p; + std::size_t wrote = 0; + for (const std::string* s : {&kStatus, &kType, &kBlank, &kBody}) { + wrote += asio::write(p.a, asio::buffer(*s)); // four separate calls + } + p.a.close(); + const std::size_t got = drain(p.b); + + std::printf("separate: buffers=4 wrote=%zu peer_read=%zu intact=%s\n", wrote, got, + wrote == got ? "yes" : "no"); + std::printf("separate: identical bytes on the wire, four times the syscalls\n"); + report("separate"); +} + +// ── topologies: who runs the handlers ──────────────────────────────────── +// +// ch49 asked how many CPUs a workload occupied; ch54 asked how many threads +// fibers migrated across. Same question, same instrument (gettid()), applied +// to an I/O runtime -- and in Asio the answer is entirely your choice of how +// you call run(). + +std::mutex g_tid_mutex; +std::set g_tids; + +void note_tid() { + const std::lock_guard lock(g_tid_mutex); + g_tids.insert(static_cast(gettid())); +} + +constexpr int kTopoPosts = 400; +constexpr int kTopoThreads = 4; + +void case_topology(const std::string& shape) { + g_tids.clear(); + + if (shape == "one") { + // One thread, one context. Every handler runs on the caller. + asio::io_context ioc; + for (int i = 0; i < kTopoPosts; ++i) { + asio::post(ioc, ¬e_tid); + } + ioc.run(); + } else if (shape == "pool") { + // N threads all calling run() on ONE context: a shared queue. + asio::io_context ioc; + for (int i = 0; i < kTopoPosts; ++i) { + asio::post(ioc, ¬e_tid); + } + run_on_threads(ioc); + } else if (shape == "percore") { + // One context per thread: no sharing, no cross-thread handoff. + std::vector threads; + threads.reserve(kTopoThreads); + for (int t = 0; t < kTopoThreads; ++t) { + threads.emplace_back([] { + asio::io_context ioc; + for (int i = 0; i < kTopoPosts / kTopoThreads; ++i) { + asio::post(ioc, ¬e_tid); + } + ioc.run(); + }); + } + for (auto& t : threads) { + t.join(); + } + } else if (shape == "threadpool") { + // thread_pool owns its threads: no run() call anywhere. + asio::thread_pool pool(kTopoThreads); + for (int i = 0; i < kTopoPosts; ++i) { + asio::post(pool, ¬e_tid); + } + pool.join(); + } + + std::printf("topology-%s: handlers=%d distinct_tids=%zu\n", shape.c_str(), kTopoPosts, + g_tids.size()); + report(("topology-" + shape).c_str()); +} + +// ── cancellation ───────────────────────────────────────────────────────── +// +// ch51 measured std::stop_token: a flag the target polls, nothing thrown. +// ch52 measured boost::thread::interrupt(): an ordinary exception at defined +// points. Asio's model is a third shape again -- a cancellation_signal whose +// slot the OPERATION subscribes to, so cancellation is delivered as a +// completion with an error code rather than as an exception or a flag. +// +// The operation still completes. It completes with operation_aborted, which +// means the handler runs exactly once either way and the ownership rules +// never change. + +void case_cancel() { + asio::io_context ioc; + asio::cancellation_signal sig; + + boost::system::error_code seen; + bool handler_ran = false; + + asio::steady_timer timer(ioc, std::chrono::hours(1)); + timer.async_wait(asio::bind_cancellation_slot(sig.slot(), + [&](const boost::system::error_code& ec) { + handler_ran = true; + seen = ec; + })); + + // Cancel before running: the operation is already queued. + sig.emit(asio::cancellation_type::all); + const std::size_t ran = ioc.run(); + + std::printf("cancel: handlers_run=%zu handler_ran=%s error=%s aborted=%s\n", ran, + handler_ran ? "yes" : "no", seen.message().c_str(), + seen == asio::error::operation_aborted ? "yes" : "no"); + std::printf("cancel: a one-hour timer completed immediately -- cancellation arrives as a " + "COMPLETION with an error code, not an exception (ch52) and not a flag (ch51)\n"); + report("cancel"); +} + +void case_versions() { + std::printf("versions: Boost %d.%d.%d asio_header_only=yes\n", BOOST_VERSION / 100000, + (BOOST_VERSION / 100) % 1000, BOOST_VERSION % 100); + report("versions"); +} + +void usage() { + std::fprintf(stderr, "usage: asiodemo \n"); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 2) { + usage(); + return 2; + } + const std::string what = argv[1]; + + if (what == "versions") { + case_versions(); + } else if (what == "work-guard") { + case_work_guard(); + } else if (what == "strand") { + case_strand(); + } else if (what == "nostrand") { + case_nostrand(); + } else if (what == "strand-cost") { + case_strand_cost(); + } else if (what == "mutex-cost") { + case_mutex_cost(); + } else if (what == "gather") { + case_gather(); + } else if (what == "separate") { + case_separate(); + } else if (what == "cancel") { + case_cancel(); + } else if (what.rfind("topology-", 0) == 0) { + case_topology(what.substr(9)); + } else { + usage(); + return 2; + } + return 0; +} diff --git a/examples/55-boost-asio/demo.sh b/examples/55-boost-asio/demo.sh new file mode 100755 index 0000000..fc2c87d --- /dev/null +++ b/examples/55-boost-asio/demo.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +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/55-boost-asio/verify.lua b/examples/55-boost-asio/verify.lua new file mode 100644 index 0000000..7c8dd89 --- /dev/null +++ b/examples/55-boost-asio/verify.lua @@ -0,0 +1,213 @@ +-- Verify 55-boost-asio (C++ only): what a library adds on top of a reactor. +-- +-- ch09 built an epoll loop by hand, ch22 wrote the nonblocking/EAGAIN state +-- machine, ch27 drove a hand-rolled coroutine reactor over epoll. Those three +-- own the readiness model and NOTHING here repeats it. This example is about +-- the concepts Asio has that a hand-written loop does not. +-- +-- NOTHING HERE IS TIMED (see ch39) and nothing binds a port -- the I/O cases +-- use an AF_UNIX socketpair, so the example is local, offline, unprivileged. +-- +-- Hard-gated (A-F): +-- A. Every case computes the same digest, 0x481984990deee5ff -- ch46-ch54. +-- B. Work guard: a fresh io_context::run() returns after ZERO handlers, +-- because run() returns on "no outstanding work" rather than on +-- completion. With an executor_work_guard held it runs the posted +-- handler instead. The single most common Asio surprise, as two counts. +-- C. THE STRAND. The same handler, the same 8 threads, posted two ways: +-- * through a strand -> the unsynchronized counter is EXACTLY right, +-- max_inflight is 1, and overlaps is 0 +-- * straight to the io_context -> handlers demonstrably overlap +-- Note carefully what is and is not asserted on the unguarded side. The +-- counter there is a data race, which is UB, and the number of lost +-- updates is not reproducible -- a run that loses nothing is legal. So +-- the gate asserts OVERLAP, which is measured with atomics and is well +-- defined, and merely REPORTS the counter. (ch49's rule: never gate on +-- the output of undefined behavior.) +-- D. A strand is not a mutex. Both produce the correct 20000, but the mutex +-- BLOCKS a thread on contention while the strand DEFERS a handler and +-- returns the thread to the pool. Counted with ch51's method +-- (strace -f -c -e trace=futex). Gated as a ratio, never as magnitudes. +-- Degrades to an informational SKIP if strace cannot attach. +-- E. Scatter-gather: four buffers written as one SEQUENCE cost exactly one +-- socket write-family syscall; the same four written separately cost +-- four. Deterministic counts, and the same readv/writev machinery Part 5 +-- covered. The gate counts sendmsg+sendto+writev and ignores plain +-- write(2), which is this program's own stdout. +-- F. Topologies: who actually runs the handlers, by gettid() -- ch50's +-- instrument and ch49's vocabulary applied to an I/O runtime. One thread +-- on one context reports exactly 1 tid; the three multi-threaded shapes +-- report more than 1. +-- +-- Gated-if-present (G): clang++ parity on the digest. + +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("55-boost-asio is C++-only (LSP_LANG=" .. tostring(lang) .. ")") +end + +local function lit(s) + return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")) +end + +local function tool_present(cmd) + return checks.run("command -v " .. cmd .. " >/dev/null 2>&1").exit == 0 +end + +local function field(out, name) + local v = out:match(name .. "=(%-?%d+)") + return v and tonumber(v) or nil +end + +local BIN = "cpp/build/release/asiodemo" +local EXPECTED_DIGEST = "digest=0x481984990deee5ff" + +local b = checks.run("./demo.sh cpp build") +checks.expect_exit(b, 0, "cpp: builds against system Boost.Asio (header-only + boost_system)") + +-- ── A. one digest, every case ──────────────────────────────────────────── + +local cases = { "versions", "work-guard", "strand", "nostrand", "strand-cost", "mutex-cost", + "gather", "separate", "cancel", "topology-one", "topology-pool", + "topology-percore", "topology-threadpool" } +local out = {} +for _, c in ipairs(cases) do + local r = checks.run("timeout 180 " .. BIN .. " " .. c) + checks.expect_exit(r, 0, "cpp: case " .. c .. " runs") + checks.expect_match(r.out, lit(EXPECTED_DIGEST), + "cpp: case " .. c .. " computes the expected digest (== ch46-ch54's)") + out[c] = r.out +end + +-- ── B. the work guard ──────────────────────────────────────────────────── + +checks.expect_match(out["work-guard"], "without_work_handlers=0", + "cpp: a fresh io_context::run() returns after ZERO handlers -- it returns on no " .. + "outstanding work, not on completion") +checks.expect_match(out["work-guard"], "guard_kept_it_alive=yes", + "cpp: an executor_work_guard keeps run() alive until the guard is released") + +-- ── C. the strand ──────────────────────────────────────────────────────── + +local st = out["strand"] +checks.expect_match(st, "correct=yes", + "cpp: through a strand, an UNSYNCHRONIZED counter incremented by 8 threads is exactly " .. + "right (counter=" .. tostring(field(st, "counter")) .. ")") +checks.expect_match(st, "max_inflight=1", + "cpp: never more than one strand handler in flight, across 8 threads calling run()") +checks.expect_match(st, "overlaps=0", + "cpp: and zero overlapping entries -- serialization without a mutex") + +local ns = out["nostrand"] +local ns_overlaps = field(ns, "overlaps") +local ns_inflight = field(ns, "max_inflight") +checks.expect_match(tostring(ns_overlaps ~= nil and ns_overlaps > 0), "true", + "cpp: the SAME handler posted straight to the io_context overlaps (" .. + tostring(ns_overlaps) .. " overlapping entries, max_inflight=" .. tostring(ns_inflight) .. + ") -- only the executor changed") +-- The lost-increment count is REPORTED by the program and deliberately not +-- asserted here: an unsynchronized increment is a data race, so losing zero on +-- some run is legal and gating on the magnitude would be gating on UB. +print("info: the unguarded counter's lost-update count is NOT gated -- an unsynchronized " .. + "increment is a data race and therefore UB, so a run that loses nothing is legal. " .. + "The gate asserts OVERLAP, which is measured with atomics and well defined.") + +-- ── D. a strand is not a mutex ─────────────────────────────────────────── + +checks.expect_match(out["strand-cost"], "correct=yes", + "cpp: the strand arm produced the correct 20000") +checks.expect_match(out["mutex-cost"], "correct=yes", + "cpp: the mutex arm produced the correct 20000 too -- both are CORRECT") + +local function futex_count(case) + local r = checks.run("strace -f -c -e trace=futex " .. BIN .. " " .. case .. + " 2>&1 | awk '/futex/{print $4}' | head -1") + return tonumber((r.out:gsub("%s", ""))) +end + +if not tool_present("strace") then + print("SKIP: strace not found -- gate D (strand vs mutex futex cost) not asserted") +else + local n_strand = futex_count("strand-cost") + local n_mutex = futex_count("mutex-cost") + if n_strand == nil or n_mutex == nil then + print("SKIP: strace could not count syscalls (ptrace-restricted?) -- gate D not asserted") + else + checks.expect_match(tostring(n_strand < n_mutex / 2), "true", + "cpp: the strand reached the kernel far less often than the mutex for identical work " .. + "(strand=" .. tostring(n_strand) .. " futex calls, mutex=" .. tostring(n_mutex) .. + ") -- a mutex BLOCKS a thread, a strand DEFERS a handler") + end +end + +-- ── E. scatter-gather ──────────────────────────────────────────────────── +-- +-- Count only socket-directed writes. Plain write(2) is this program's own +-- stdout and the runtime's internal pipe, and including it would make the +-- comparison meaningless. + +local function socket_writes(case) + local r = checks.run("strace -f -c -e trace=writev,sendmsg,sendto " .. BIN .. " " .. case .. + " 2>&1 | awk '/writev|sendmsg|sendto/{s+=$4} END{print s+0}'") + return tonumber((r.out:gsub("%s", ""))) +end + +if not tool_present("strace") then + print("SKIP: strace not found -- gate E (scatter-gather syscall counts) not asserted") +else + local n_gather = socket_writes("gather") + local n_separate = socket_writes("separate") + checks.expect_match(tostring(n_gather == 1), "true", + "cpp: four buffers written as ONE sequence cost exactly one socket write-family " .. + "syscall (got " .. tostring(n_gather) .. ") -- the kernel's iovec form, reached " .. + "without assembling one by hand") + checks.expect_match(tostring(n_separate == 4), "true", + "cpp: the identical four buffers written separately cost four (got " .. + tostring(n_separate) .. ")") + checks.expect_match(out["gather"], "intact=yes", + "cpp: and the peer read back exactly the bytes that were written") +end + +-- ── F. topologies ──────────────────────────────────────────────────────── + +checks.expect_match(out["topology-one"], "distinct_tids=1", + "cpp: one thread calling run() on one io_context ran every handler itself") + +for _, shape in ipairs({ "pool", "percore", "threadpool" }) do + local o = out["topology-" .. shape] + local n = field(o, "distinct_tids") + checks.expect_match(tostring(n ~= nil and n > 1), "true", + "cpp: topology-" .. shape .. " spread handlers across " .. tostring(n) .. + " OS threads -- same handlers, different shape of run()") +end + +-- ── cancellation: a completion, not an exception or a flag ─────────────── + +checks.expect_match(out["cancel"], "aborted=yes", + "cpp: a cancelled operation completes with operation_aborted -- the handler still runs " .. + "exactly once, so ownership rules never change") +checks.expect_match(out["cancel"], "handlers_run=1", + "cpp: and a one-hour timer's handler ran immediately rather than being dropped") + +-- ── G. clang parity (skip-if-absent) ───────────────────────────────────── + +if tool_present("clang++") then + local cl = checks.run( + "cd cpp && cmake --preset release-clang >/dev/null 2>&1 && " .. + "cmake --build --preset release-clang --target asiodemo >/dev/null 2>&1 && " .. + "timeout 180 ./build/release-clang/asiodemo strand") + checks.expect_exit(cl, 0, "cpp: clang-built asiodemo runs") + checks.expect_match(cl.out, lit(EXPECTED_DIGEST), + "cpp: clang and GCC agree on the digest") + checks.expect_match(cl.out, "overlaps=0", + "cpp: and clang's build serializes the strand identically") +else + print("SKIP: clang++ not found on PATH -- gate G not asserted") +end + +checks.finish() diff --git a/examples/manifest.yaml b/examples/manifest.yaml index 1cc647a..dfa3aba 100644 --- a/examples/manifest.yaml +++ b/examples/manifest.yaml @@ -231,3 +231,9 @@ examples: langs: [cpp] mode: local timeout: 900 + # 13 subcommands, several spinning up 8 threads, plus four straced runs. + # Uses an AF_UNIX socketpair -- nothing binds a port, so still mode: local. + - id: 55-boost-asio + langs: [cpp] + mode: local + timeout: 900