Skip to content

Commit aded623

Browse files
docs(r23): Boost.Fiber — example 54 + chapter 54 (Compendium: C++ Concurrency)
Sixth chapter of Part 14, and the stackful counterpart to ch53. ch53 measured a stackless coroutine frame at 32 bytes and noted in passing that co_await only works inside a coroutine body. ch54 is about what a real stack costs and what that restriction is worth escaping. The memory trilogy, three instruments across three chapters: thread 8388608 B pthread_getattr_np (ch50) fiber 131072 B stack_traits::default_size() (ch54) coroutine frame 32 B operator new in promise (ch53) 8 MiB -> 128 KiB -> 32 bytes. Both ratios (64, 4096) computed in the program from those measured numbers, never quoted. What the stack buys, demonstrated rather than described: a fiber suspends from inside an ORDINARY non-coroutine function six frames deep and resumes at the same depth. A stackless coroutine cannot express this at any price — co_await colours every frame in the suspend path. The guard page, both directions: protected_fixedsize_stack costs exactly one page more than fixedsize_stack (delta gated against page_size(), not a hardcoded 4096), and recursing 60x past a 64 KiB stack dies with exit 139 (SIGSEGV) rather than corrupting the next mapping. The gate asserts the exit SIGNAL only — output after a stack overflow is undefined — and explicitly rejects 124, since a hung run must not pass a test for faulting promptly. The scheduler is a choice: the same 16 fibers report distinct_tids=1 under the default round_robin and 4 under shared_work, one call to use_scheduling_algorithm apart. That is ch49's grid selected by a line, where ch44's Go runtime decides for you. The TU is compiled -O0 deliberately and the reason is in CMakeLists.txt: at -O2 the recursion can become a loop and never touch the guard page, which would make guard-overflow exit 0 and quietly stop proving anything. ch49's optimizer lesson, one layer down. Two diagrams (Fig 54.1 three ways to pause, Fig 54.2 the scheduler is a choice). System Boost 1.90.0 — no Conan, no network. Verified on the Fedora 44 host: verify.lua PASS 32 / FAIL 0 with the migration, fault, and clang gates all running for real; runner 1 passed; validate.py OK.
1 parent b6e4300 commit aded623

17 files changed

Lines changed: 3977 additions & 0 deletions

‎_docs/54-boost-fiber.md‎

Lines changed: 447 additions & 0 deletions
Large diffs are not rendered by default.

‎_plans/r23-ch54-plan.md‎

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
---
2+
title: "r23 / ch54 — Boost.Fiber — plan (internal)"
3+
published: false
4+
---
5+
6+
# r23 ch54 — example `54-boost-fiber` + chapter + diagrams
7+
8+
Sixth chapter of Part 14.
9+
10+
## Overlap check (done FIRST, as in r22)
11+
12+
- **ch44** covers Go's GMP: M:N scheduling, work-stealing, netpoller, 2000 goroutines on
13+
~12 threads. So ch54 must not re-derive M:N *generically* — it must show C++'s version
14+
and what is different about choosing it explicitly.
15+
- **ch53** measured a stackless coroutine frame (32 B) and the HALO split.
16+
- **ch27** owns the coroutine/epoll mechanism.
17+
- `grep -il fiber _docs/*.md` → only outline/ch49/ch50 forward-references, plus ch35
18+
(unrelated, KVM). **No prior chapter covers stackful context switching.**
19+
20+
## The spine: stackful is the whole difference
21+
22+
ch53 established that a coroutine frame is 32 bytes and holds only what crosses a
23+
suspension. A fiber has a **real stack**. That single fact produces both the cost and
24+
the capability, and it completes Part 14's memory story:
25+
26+
| paused computation | per-unit cost | measured in |
27+
| --- | --- | --- |
28+
| thread | **8388608 B** (8 MiB) | ch50 |
29+
| **fiber** | **131072 B** (128 KiB) | ch54 |
30+
| coroutine frame | **32 B** | ch53 |
31+
32+
8 MiB → 128 KiB → 32 B. Roughly two orders of magnitude at each step, every number
33+
measured on this host rather than quoted.
34+
35+
## Host audit (2026-08-20, Fedora 44, Boost 1.90.0, GCC 16.1.1)
36+
37+
`boost-devel-1.90.0-7.fc44`; `libboost_fiber.so.1.90.0` and `libboost_context.so.1.90.0`
38+
present. **No Conan, no network.** Link: `-lboost_fiber -lboost_context -pthread`.
39+
40+
| fact | measured |
41+
| --- | --- |
42+
| `ctx::stack_traits::default_size()` | **131072** (128 KiB) |
43+
| `ctx::stack_traits::minimum_size()` | **14528** |
44+
| `ctx::stack_traits::page_size()` | 4096 |
45+
| `is_unbounded()` | yes |
46+
| `fixedsize_stack(64 KiB).allocate().size` | **65536** — exactly what was asked |
47+
| `protected_fixedsize_stack(64 KiB)` | **69632** = 65536 + one 4096 guard page |
48+
| fiber runs on | the **calling thread's tid** (same as main) |
49+
50+
### Guard page, both directions (measured)
51+
52+
| case | result |
53+
| --- | --- |
54+
| recurse 5 frames on a 64 KiB protected stack | completes, exit 0 |
55+
| recurse 1000 frames (~4 MiB) on the same stack | **SIGSEGV, exit 139** |
56+
57+
The guard page turns a stack overflow into an immediate fault instead of silent
58+
corruption of whatever is mapped next. This is the ch49 `oob` pattern one layer up.
59+
60+
### M:N, and the scheduler is a choice (measured)
61+
62+
Same 16 fibers, same work, two schedulers, tids collected with `gettid()`:
63+
64+
| scheduler | distinct tids |
65+
| --- | --- |
66+
| default `round_robin` | **1** |
67+
| `algo::shared_work` (4 threads) | **4** |
68+
69+
This is **ch49's grid applied to fibers**: identical concurrency structure, and whether
70+
any parallelism happens is one `use_scheduling_algorithm` call. Unlike Go, where the
71+
runtime decides, C++ makes you choose — and the choice is observable.
72+
73+
### The capability the stack buys (measured)
74+
75+
A fiber can suspend from **deep inside an ordinary call tree**:
76+
77+
```
78+
deep: yielding from depth=6 inside a plain function
79+
deep: resumed at depth=6
80+
```
81+
82+
`leaf()` is not a coroutine and neither is anything above it. `co_await` is only valid
83+
in the body of a coroutine, so a stackless coroutine fundamentally cannot do this — every
84+
frame in the suspend path must itself be a coroutine ("function colouring"). The fiber's
85+
128 KiB is what buys the ability to suspend anywhere.
86+
87+
## The core demo — `fiberdemo` (C++-only, `langs: [cpp]`)
88+
89+
Subcommands: `stacks` (traits + three allocators), `versus` (the 3-way memory table),
90+
`deep` (suspend from a plain function 6 frames down), `roundrobin` / `sharedwork` (tid
91+
counts), `guard-ok` / `guard-overflow` (the fault), `versions`.
92+
93+
`guard-overflow` dies of SIGSEGV **by design** — its own subcommand, invoked only under
94+
the harness's expectation of failure, exactly as ch50's `cancel-swallow` and ch51's
95+
`deadlock-naive` were.
96+
97+
Digest continuity: `digest=0x481984990deee5ff`.
98+
99+
## Gate tiers
100+
101+
**Hard (A–F):**
102+
- A. digest identical across every non-faulting subcommand.
103+
- B. stacks: default is 131072; a 64 KiB request yields exactly 65536; the protected
104+
allocator adds exactly one page (69632 - 65536 == 4096 == `page_size()`). Gate the
105+
page-delta relationship, not the absolute default.
106+
- C. versus: fiber stack sits strictly between ch53's 32 B frame and ch50's 8388608 B
107+
thread stack, and both ratios are computed in the program.
108+
- D. deep: a plain non-coroutine function suspends and resumes at depth > 1.
109+
- E. M:N: `roundrobin` reports exactly 1 distinct tid; `sharedwork` reports > 1. Same
110+
fibers, so the scheduler is the only variable.
111+
- F. guard: in-budget completes (exit 0); overflow dies of a signal (exit >= 128), and
112+
the gate asserts the *signal*, never any output the faulting run produced.
113+
114+
**Gated-if-present (G):** clang parity.
115+
116+
**Not gated:** context-switch timings (ch39); the exact `minimum_size()`; anything
117+
requiring more than one core for correctness.
118+
119+
## Risks
120+
121+
- **`sharedwork` tid count is scheduler-dependent.** Measured 4/4 here, but gate `> 1`
122+
and never `== 4`; a loaded host could legitimately keep the work on fewer threads.
123+
Bounded retry if it comes back 1, as ch49's gate B did.
124+
- **`guard-overflow` must fault, not hang.** Recursion depth 1000 × ~4 KiB is ~4 MiB
125+
against a 64 KiB stack — 60x past it, so it cannot accidentally fit. Run under
126+
`timeout` anyway.
127+
- **Never assert on what the faulting run printed.** Same rule as ch49's `oob_unchecked`:
128+
the output after a stack overflow is undefined. Assert the exit signal only.
129+
- **`-O0` for the guard case**: an optimizing compiler can turn the recursion into a
130+
loop and never touch the guard page. Force `-O0` on that translation unit and say why.
131+
- **Do not re-derive ch44.** ch54 shows C++'s explicit scheduler choice; ch44 owns Go's
132+
automatic one. Cite it, contrast it, do not re-explain M:N from scratch.
133+
134+
## Steps
135+
136+
S1 mint + strip · S2 `cpp/src/fiberdemo.cpp` + CMakeLists · S3 `verify.lua` · S4 manifest ·
137+
S5 transcripts · S6 `_docs/54-boost-fiber.md` · S7 2 diagrams + catalogue · S8 gate matrix ·
138+
S9 PR, stop there.
139+
140+
## Acceptance
141+
142+
`verify.lua` PASS/FAIL 0 with E and F running for real; runner 1 passed; `validate.py` OK;
143+
every chapter code block verbatim; footer states Boost 1.90.0, the three stack numbers,
144+
the guard-page delta, and the two tid counts as measured.
145+
146+
## Outcome (2026-08-20)
147+
148+
All steps complete. `verify.lua` → **PASS 32 / FAIL 0** with gates E (migration), F (the
149+
guard-page fault) and G (clang parity) all running for real. Runner → 1 passed.
150+
`validate.py` → OK. Banned-words clean. **19/19 chapter code blocks verbatim**; all 7
151+
`Chapter N` cross-references resolve.
152+
153+
### The overlap check (second time doing this before planning, and it paid again)
154+
155+
`grep -il fiber _docs/*.md` found only forward-references. ch44 owns Go's *automatic*
156+
M:N; ch54 shows C++'s *explicit* one and contrasts rather than re-explains. ch53 owns
157+
stackless; ch54 is the stackful counterpart and the two compose into the trilogy.
158+
159+
### Measured, and worth keeping
160+
161+
| paused computation | bytes | instrument | chapter |
162+
| --- | --- | --- | --- |
163+
| thread | 8388608 | `pthread_getattr_np` | ch50 |
164+
| fiber | 131072 | `stack_traits::default_size()` | ch54 |
165+
| coroutine frame | 32 | `operator new` in `promise_type` | ch53 |
166+
167+
Three different instruments, three chapters, one ordering — and the ordering falls out of
168+
structure rather than benchmark noise. Both ratios (64, 4096) are computed in the program.
169+
170+
Guard page: `fixedsize_stack(64 KiB)` → exactly 65536; `protected_fixedsize_stack(64 KiB)`
171+
→ 69632, delta **4096 == `page_size()`**. Overflow 60x past it → **exit 139 (SIGSEGV)**.
172+
173+
Scheduler: same 16 fibers → `round_robin` **1 tid**, `shared_work` **4 tids**, stable at
174+
4/4 across five runs.
175+
176+
Deep suspend: yielded and resumed at **depth 6** inside ordinary functions.
177+
178+
### Deltas
179+
180+
- **`-O0` forced on the translation unit**, and documented in `CMakeLists.txt` rather
181+
than left implicit. At `-O2` the recursion can become a loop and never reach the guard
182+
page, which would make `guard-overflow` exit 0 and silently stop demonstrating
183+
anything — ch49's optimizer lesson, one layer down. The `volatile` array is the same
184+
defence.
185+
- **Gate F rejects exit 124 explicitly.** `timeout(1)` returns 124 when it kills
186+
something, and a *hung* run must not pass a gate that means "faulted promptly".
187+
- **Guard delta gated against `page_size()`**, not a hardcoded 4096, so it holds on a
188+
host with different page sizes.
189+
- **`fiber_specific_ptr` header path corrected** before shipping: it is in
190+
`<boost/fiber/fss.hpp>`, not `fiber_specific_ptr.hpp`. Verified by grep rather than
191+
assumed. All three named-but-unexercised APIs were confirmed present on this host and
192+
the footer says they are named, not gated.

0 commit comments

Comments
 (0)