From 578bd46ec1d15968ad5da3ed954fc5fca4ec1470 Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Mon, 6 Jul 2026 11:17:32 -0600 Subject: [PATCH] docs: document io_context teardown precondition (#298) Document that an io_context must outlive every post/dispatch through its executor, that no thread may be inside a run variant at destruction, and that outstanding work must be drained before the context is destroyed. Added to the io_context class docstring, its executor's dispatch()/post() operations, and the I/O Context guide. --- .../ROOT/pages/4.guide/4c.io-context.adoc | 29 +++++++++++++++++++ include/boost/corosio/io_context.hpp | 27 +++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc index a0d85a47a..3657d09d9 100644 --- a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc @@ -266,6 +266,35 @@ work across threads. WARNING: Individual I/O objects (sockets, timers) are not thread-safe. Don't access the same socket from multiple threads without synchronization. +== Lifetime and Teardown + +The `io_context` must outlive every operation posted or dispatched through its +executor, and no thread may still be inside a `run()` call when the context is +destroyed. Posting to the context — from a thread it does not track — +concurrently with, or after, its destruction is undefined behavior. + +Work launched with `capy::run` / `capy::run_async` is work-tracked, so a normal +`run()` completion already waits for it to finish. The safe teardown pattern is +therefore: stop submitting new work, let every `run()` return, join the threads +that ran the loop, and only then destroy the context. + +[source,cpp] +---- +corosio::io_context ioc(4); + +std::vector threads; +for (int i = 0; i < 4; ++i) + threads.emplace_back([&ioc] { ioc.run(); }); + +// Join every run() thread before ioc leaves scope. +for (auto& t : threads) + t.join(); +---- + +WARNING: `stop()` abandons pending work rather than draining it. Destroying the +context after `stop()` while other threads may still post to it is undefined +behavior — join those threads first. + == Inheritance from execution_context `io_context` inherits from `capy::execution_context`, providing service diff --git a/include/boost/corosio/io_context.hpp b/include/boost/corosio/io_context.hpp index 275d37046..44fc6f6c0 100644 --- a/include/boost/corosio/io_context.hpp +++ b/include/boost/corosio/io_context.hpp @@ -182,6 +182,18 @@ class timer_service; io_context ioc2(corosio::epoll); // explicit backend @endcode + @par Preconditions + The context must outlive every operation posted or dispatched + through its executor, and no thread may be executing a run + variant when the context is destroyed. Posting to the context + concurrently with, or after, its destruction is undefined + behavior. The safe teardown pattern is to stop submitting new + work, let every `run()` call return (each returns once no + outstanding work remains), and join the threads that ran the + loop before destroying the context. Work launched with + `capy::run` / `capy::run_async` is work-tracked, so a normal + `run()` completion already waits for it. + @par Thread Safety Distinct objects: Safe.@n Shared objects: Safe, if using a concurrency hint greater @@ -572,6 +584,11 @@ class io_context::executor_type @param c The continuation to dispatch. @return A handle for symmetric transfer or `std::noop_coroutine()`. + + @par Preconditions + The associated context must outlive this call. Dispatching + concurrently with, or after, the context's destruction is + undefined behavior. */ std::coroutine_handle<> dispatch(capy::continuation& c) const { @@ -585,6 +602,11 @@ class io_context::executor_type Enqueues `c` directly on the scheduler's ready queue. No heap allocation occurs. + + @par Preconditions + The associated context must outlive this call. Posting + concurrently with, or after, the context's destruction is + undefined behavior. */ void post(capy::continuation& c) const { @@ -598,6 +620,11 @@ class io_context::executor_type the `post(scheduler_op*)` overload to avoid the allocation. @param h The coroutine handle to post. + + @par Preconditions + The associated context must outlive this call. Posting + concurrently with, or after, the context's destruction is + undefined behavior. */ void post(std::coroutine_handle<> h) const {