From 5082702eff038f96c56192309d73f603d302f168 Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Thu, 2 Jul 2026 10:43:12 -0600 Subject: [PATCH] docs: document strand/thread_pool teardown precondition (#348) A strand holds only a non-owning reference to its execution context. Posting or dispatching to a strand concurrently with, or after, that context's destruction is undefined behavior: it races service teardown (the #348 null-deref of service_) and, past destroy(), a use-after-free on the freed service state and pool queue. This is a precondition violation, not a bug reachable by correct code: legitimate posts run on pool workers, and join()-before-shutdown() already ensures no worker is active when the service tears down. The safe pattern is to submit work through run/run_async (work-tracked, so join() waits for it) and to join() the context before destroying it. Document the requirement on strand (class + post()/dispatch()) and on the thread_pool destructor. No code change. --- include/boost/capy/ex/strand.hpp | 23 +++++++++++++++++++++++ include/boost/capy/ex/thread_pool.hpp | 7 +++++++ 2 files changed, 30 insertions(+) diff --git a/include/boost/capy/ex/strand.hpp b/include/boost/capy/ex/strand.hpp index 6a22f3bb0..a84a40857 100644 --- a/include/boost/capy/ex/strand.hpp +++ b/include/boost/capy/ex/strand.hpp @@ -50,6 +50,19 @@ namespace capy { - `dispatch(continuation&)` - May run immediately if already executing in this strand - `post(continuation&)` - Always queues for later execution + @par Preconditions + A strand holds only a non-owning reference to its inner executor's + execution context (for example a `thread_pool`). That context must + outlive every post() and dispatch() call; posting or dispatching + concurrently with, or after, the context's destruction is undefined + behavior. To guarantee this, submit work through @ref run_async or + @ref run — whose operations are work-tracked, so the context's + `join()` waits for them — and call `join()` on the context before + destroying it, rather than posting to a strand from an external + thread the context does not track. Destroying the strand handle + itself is always safe, including after the context has been + destroyed. + @par Thread Safety Distinct objects: Safe. Shared objects: Safe. @@ -218,6 +231,11 @@ class strand @param c The continuation to post. The caller retains ownership; the continuation must remain valid until it is dequeued and resumed. + + @par Preconditions + The strand's execution context must outlive this call. Posting + concurrently with, or after, that context's destruction is + undefined behavior. */ void post(continuation& c) const @@ -241,6 +259,11 @@ class strand it is dequeued and resumed. @return A handle for symmetric transfer or `std::noop_coroutine()`. + + @par Preconditions + The strand's execution context must outlive this call. + Dispatching concurrently with, or after, that context's + destruction is undefined behavior. */ std::coroutine_handle<> dispatch(continuation& c) const diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp index 6aa377296..ef8586d3b 100644 --- a/include/boost/capy/ex/thread_pool.hpp +++ b/include/boost/capy/ex/thread_pool.hpp @@ -60,6 +60,13 @@ class BOOST_CAPY_DECL Signals all worker threads to stop, waits for them to finish, and destroys any pending work items. + + @par Preconditions + No thread outside this pool may post or dispatch work to it + (or to a strand built on it) concurrently with, or after, + destruction; doing so is undefined behavior. Submit such work + through @ref run_async or @ref run and call @ref join before + the pool is destroyed, so it has completed first. */ ~thread_pool();