A shared seam needs a thread-safe backend (L0/L1 substrate)¶
Applies to builds where tr::mem::kSpinWaitSafe is true
This is the one build-configuration-dependent example in the wire and view domains. A
single-core priority-preemptive target sets tr::mem::kSpinWaitSafe = false in
libtracer/config_override.hpp; there a spinner that outranks the lock holder never yields the
CPU the holder needs, so binding spin_sync_t is not “slower” — it is a hang, and
synchronized_pool_t refuses the instantiation outright. The target is still always built and
always linked: it follows the binding with if constexpr, prints
skipped: this build sets tr::mem::kSpinWaitSafe = false … and passes.
An example a binding does not apply to must skip at run time, never fail to compile. The
mechanics differ from sub_unsubscribe_from_dispatch, and
the difference is a two-layer trap worth stating outright. First, if constexpr alone is not
enough: in a non-template function the discarded branch is still fully instantiated, so
declaring the pool inside a discarded branch of main would trip the assert regardless.
Second — and this is the one that is easy to miss — being a template is not enough either:
a template’s non-dependent constructs are checked at definition time, so spelling the
concrete tr::mem::sync_pool_t inside the discarded branch trips it too. The pool type has to
depend on a template parameter, which is why the helper takes the sync policy as one. Line
1 of a run prints the bound value, so the output says which arm it took. Every other example in
these two domains is unconditional.
A segment self-routes its reclaim on whatever thread drops the last reference — typically a
subscriber or a transport receive thread, concurrent with a writer’s alloc. So any
mem_backend_t injected at a shared seam must tolerate that
(ADR-0060
§2): a graph_t’s value backend, a router’s flat, a transport vertex’s rx backend.
tr::mem::synchronized_pool_t is the bounded answer — one pool_t
whose O(1) free-list operations run inside a critical section.
What to notice¶
The mechanism is a compile-time policy, because only the target knows its concurrency model. A multi-core host wants the
spin_sync_tspinlock: negligible contention on an O(1) section, and it avoids the ~2 µs OS-mutex round trip that would dominate a ~120 ns free-list operation. A single-core MCU wants an interrupt-disable critical section instead. The choice is a template argument — no branch, no vtable, no per-alloc indirection.tr::mem::sync_pool_tis the host spelling, and it is the discoverable short name — which is why a build that forbids spin-waiting rejects it loudly rather than shipping a hang.A single thread-safe pool, never per-stripe sharding. Sharding removes no race and adds partition imbalance.
ISR-safety and non-blocking are different facts. A spinlock is
is_nonblockingbut notis_isr_safe, and the example asserts the latter — the traits forward the policy’s guarantees rather than inventing them.It is opt-in construction only. No seam defaults to it;
heap_backend()remains the default everywhere.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — a backend at a SHARED seam must be thread-safe (ADR-0060 §2).
9 *
10 * A segment self-routes its reclaim on whatever thread drops the last reference — typically a
11 * subscriber or a transport receive thread, concurrent with a writer's `alloc`. So any
12 * `mem_backend_t` injected at a shared seam (a `graph_t`'s value backend, a router's flat, a
13 * transport vertex's rx backend) must tolerate that. `tr::mem::synchronized_pool_t` is the
14 * bounded answer: one `pool_t` whose O(1) free-list ops run inside a critical section, with
15 * the MECHANISM as a compile-time policy — because only the target knows its concurrency
16 * model. `tr::mem::sync_pool_t` is the multi-core-host spelling, `spin_sync_t`.
17 *
18 * THIS EXAMPLE IS BUILD-CONFIGURATION-DEPENDENT, and it is the only one in the wire/view set
19 * that is. A single-core priority-preemptive target sets `tr::mem::kSpinWaitSafe = false` in
20 * `libtracer/config_override.hpp` — there a spinner that outranks the lock holder never
21 * yields the CPU the holder needs, so binding `spin_sync_t` is a hang, and
22 * `synchronized_pool_t` refuses the instantiation. An example a binding does not apply to
23 * must SKIP AT RUN TIME, never fail to compile, so the body lives in a template: in a
24 * non-template function the discarded `if constexpr` branch is still instantiated, and
25 * declaring the pool there would trip the assert anyway. Line 1 of a run says which arm it
26 * took.
27 *
28 * Runs under ctest as `example_view_sync_pool`; returns non-zero on any failed check. Where
29 * `kSpinWaitSafe` is false it prints a skip line and passes.
30 */
31
32#include <array>
33#include <atomic>
34#include <cstddef>
35#include <cstdio>
36#include <optional>
37#include <thread>
38#include <vector>
39
40#include "libtracer/tracer.hpp"
41
42namespace {
43
44/** @brief Report expectation @p what and record a failure on @p ok. */
45void check(bool& ok, bool cond, const char* what) {
46 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
47 ok = ok && cond;
48}
49
50/** @brief Slots per thread in the churn below — enough to interleave, small enough to be quick. */
51constexpr int kRounds = 2000;
52
53/**
54 * @brief The pool exercise — a TEMPLATE so the discarded branch is never instantiated.
55 *
56 * Two things are load-bearing about this signature, and both are easy to get wrong. First,
57 * `if constexpr` alone is not enough: in a NON-template function the discarded branch is still
58 * fully instantiated, so declaring the pool inside a discarded branch of `main` would trip the
59 * assert regardless. Second, being a template is not enough EITHER — a template's
60 * NON-dependent constructs are checked at definition time, so spelling the concrete
61 * `tr::mem::sync_pool_t` here would trip it too. The type has to depend on a template
62 * parameter, which is what @p Sync is for.
63 *
64 * @tparam Enabled `tr::mem::kSpinWaitSafe`, i.e. whether this target may bind a spin-waiting
65 * critical section at all.
66 * @tparam Sync The synchronization policy to bind. Defaulted rather than hard-coded purely so
67 * that `synchronized_pool_t<Sync>` below is a DEPENDENT type and is therefore left
68 * uninstantiated when the branch is discarded.
69 * @return True when every expectation held (and trivially true on the skip arm).
70 */
71template <bool Enabled, class Sync = tr::mem::spin_sync_t>
72bool run_sync_pool() {
73 if constexpr (Enabled) {
74 alignas(std::max_align_t) std::array<std::byte, 4096> slab{};
75 tr::mem::synchronized_pool_t<Sync> pool{slab, 32}; // == tr::mem::sync_pool_t
76 std::printf("sync_pool_t over a %zu-byte slab: %zu slots, two threads\n", slab.size(),
77 pool.capacity());
78
79 std::atomic<int> served{0};
80 const auto churn = [&pool, &served] {
81 for (int i = 0; i < kRounds; ++i) {
82 // alloc on this thread, drop on this thread — but the two threads race for
83 // the same free list, which is exactly what the policy guards.
84 tr::view::segment_ptr_t seg = tr::view::segment_alloc(pool, 8);
85 if (seg) served.fetch_add(1, std::memory_order_relaxed);
86 }
87 };
88 std::thread a(churn);
89 std::thread b(churn);
90 a.join();
91 b.join();
92
93 // The free list has no `available()` counter through the synchronized facade, so the
94 // honest check is to drain it: every slot must still be reachable afterwards.
95 std::vector<tr::view::segment_ptr_t> drained;
96 while (auto seg = tr::view::segment_alloc(pool, 8)) drained.push_back(std::move(seg));
97
98 bool ok = true;
99 std::printf("%d of %d allocations served; %zu/%zu slots reachable afterwards\n",
100 served.load(), 2 * kRounds, drained.size(), pool.capacity());
101 check(ok, served.load() == 2 * kRounds, "every request was served — the slab never leaked");
102 check(ok, drained.size() == pool.capacity(),
103 "and the free list is whole: no slot was lost to a race");
104 check(ok, !tr::mem::synchronized_pool_t<Sync>::is_isr_safe,
105 "a spinlock is not an ISR critical section — the policy says so in a constant");
106 return ok;
107 } else {
108 std::printf(
109 "skipped: this build sets tr::mem::kSpinWaitSafe = false — sync_pool_t spin-waits "
110 "and this target must bind an interrupt-disable policy instead\n");
111 return true;
112 }
113}
114
115} // namespace
116
117int main() {
118 std::printf("tr::mem::kSpinWaitSafe bound by this build: %d\n",
119 static_cast<int>(tr::mem::kSpinWaitSafe));
120 const bool ok = run_sync_pool<tr::mem::kSpinWaitSafe>();
121 std::printf("RESULT %s\n", ok ? "ok" : "FAILED");
122 return ok ? 0 : 1;
123}
See also: backends · configuration · concurrency & scaling reference.