A shared seam needs a thread-safe backend (L0/L1 substrate)¶
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 guard, because only the target knows its concurrency model. Since RFC-0028 slice 10 the pool’s guard is the same
tr::guardtrait the last-known-value slot uses, andsynchronized_pool_t<>binds the build’s onetr::graph::guard_t: on a host thetr::mutex_guard_t(a short bounded spin, then a nap — never a pure spin, so it cannot hang a priority-preemptive scheduler), on ESP-IDF the interrupt-maskedcritical_guard_t(tr::esp::critical_pool_t). The choice is a template argument — no branch, no vtable, no per-alloc indirection.A guard that may pure-spin is refused where spinning is unsafe. A build that sets
tr::mem::kSpinWaitSafe = falserejects any guard declaringmay_spin = trueat the instantiation, so a spinner that outranks the lock holder is a compile error, not 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. The pool forwards its guard’s
is_isr_safe/is_nonblockingrather than inventing them, and the example checks that.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 guard — because only the target knows its concurrency
16 * model. Since RFC-0028 slice 10 the guard is the SAME trait the last-known-value slot uses
17 * (`tr::graph::guard_t`), so `synchronized_pool_t<>` binds the build's one guard: the
18 * host `mutex_guard_t` (a short bounded spin, then a nap — never a pure spin, so it is safe on
19 * every scheduler), or an ESP-IDF build's interrupt-masked `critical_guard_t`.
20 *
21 * Runs under ctest as `example_view_sync_pool`; returns non-zero on any failed check.
22 */
23
24#include <array>
25#include <atomic>
26#include <cstddef>
27#include <cstdio>
28#include <thread>
29#include <vector>
30
31#include "libtracer/tracer.hpp"
32
33namespace {
34
35/** @brief Report expectation @p what and record a failure on @p ok. */
36void check(bool& ok, bool cond, const char* what) {
37 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
38 ok = ok && cond;
39}
40
41/** @brief Slots per thread in the churn below — enough to interleave, small enough to be quick. */
42constexpr int kRounds = 2000;
43
44/** @brief The pool exercise: two threads race one free list through the synchronized pool. */
45bool run_sync_pool() {
46 alignas(std::max_align_t) std::array<std::byte, 4096> slab{};
47 tr::mem::synchronized_pool_t<> pool{slab, 32}; // the build's one guard
48 std::printf("synchronized_pool_t<%s> over a %zu-byte slab: %zu slots, two threads\n",
49 tr::graph::guard_t::name, slab.size(), pool.capacity());
50
51 std::atomic<int> served{0};
52 const auto churn = [&pool, &served] {
53 for (int i = 0; i < kRounds; ++i) {
54 // alloc on this thread, drop on this thread — but the two threads race for the
55 // same free list, which is exactly what the guard protects.
56 tr::view::segment_ptr_t seg = tr::view::segment_alloc(pool, 8);
57 if (seg) served.fetch_add(1, std::memory_order_relaxed);
58 }
59 };
60 std::thread a(churn);
61 std::thread b(churn);
62 a.join();
63 b.join();
64
65 // The free list has no `available()` counter through the synchronized facade, so the
66 // honest check is to drain it: every slot must still be reachable afterwards.
67 std::vector<tr::view::segment_ptr_t> drained;
68 while (auto seg = tr::view::segment_alloc(pool, 8)) drained.push_back(std::move(seg));
69
70 bool ok = true;
71 std::printf("%d of %d allocations served; %zu/%zu slots reachable afterwards\n", served.load(),
72 2 * kRounds, drained.size(), pool.capacity());
73 check(ok, served.load() == 2 * kRounds, "every request was served — the slab never leaked");
74 check(ok, drained.size() == pool.capacity(),
75 "and the free list is whole: no slot was lost to a race");
76 check(ok, tr::mem::synchronized_pool_t<>::is_isr_safe == tr::graph::guard_t::is_isr_safe,
77 "the pool forwards its guard's guarantees rather than inventing them");
78 return ok;
79}
80
81} // namespace
82
83int main() {
84 const bool ok = run_sync_pool();
85 std::printf("RESULT %s\n", ok ? "ok" : "FAILED");
86 return ok ? 0 : 1;
87}
See also: backends · configuration · concurrency & scaling reference.