From 5dd6dd9b8c2074073e22b3251ebf3878807f7a62 Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Sat, 17 Oct 2026 09:02:42 -0700 Subject: [PATCH] docs(signal_set): drop the Windows inline-resume note on stop A stop request on IOCP no longer resumes the waiter inline on the request_stop() thread; since 0360d0f4 every completion is posted to the waiter's executor, as on POSIX. Remove the platform notes that still said otherwise, fix the stale comment in win_signal::wait, and add a test that a cross-thread stop resumes the wait on the context's thread. Closes #394 --- .../ROOT/pages/4.guide/4i.signals.adoc | 5 +-- include/boost/corosio/io/io_signal_set.hpp | 4 -- .../native/detail/iocp/win_signals.hpp | 3 +- test/unit/signal_set.cpp | 43 +++++++++++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc index 245322cff..b60dabe68 100644 --- a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc +++ b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc @@ -207,9 +207,8 @@ A stop request cancels only the wait that is pending when it arrives. Unlike a stop request that arrives between waits is discarded. The following `wait()` starts uncancelled. -NOTE: On Windows the cancelled wait resumes inline, on the thread that -called `request_stop()`. On POSIX it always resumes on a thread running the -execution context. +A cancelled wait resumes on a thread running the execution context, even +when `request_stop()` is called from another thread. == Use Cases diff --git a/include/boost/corosio/io/io_signal_set.hpp b/include/boost/corosio/io/io_signal_set.hpp index 24209d704..eb174fa46 100644 --- a/include/boost/corosio/io/io_signal_set.hpp +++ b/include/boost/corosio/io/io_signal_set.hpp @@ -113,10 +113,6 @@ class BOOST_COROSIO_DECL io_signal_set : public io_object This signal set must outlive the returned awaitable. - @note On Windows a stop request resumes the awaiting coroutine - inline, on the thread that called `request_stop()`. On POSIX - it always resumes on a thread running the execution context. - @return An awaitable that completes with `results`. Returns the signal number when a signal is delivered, or an error code on failure. diff --git a/include/boost/corosio/native/detail/iocp/win_signals.hpp b/include/boost/corosio/native/detail/iocp/win_signals.hpp index 7de3680e2..99a8a478a 100644 --- a/include/boost/corosio/native/detail/iocp/win_signals.hpp +++ b/include/boost/corosio/native/detail/iocp/win_signals.hpp @@ -416,7 +416,8 @@ win_signal::wait( stop_cb_.emplace(token, token_canceller{this}); svc_.start_wait(*this, &pending_op_); - // cancellation resumes inline here too; only signal delivery is posted. + // Every completion -- delivery, cancel() or a stop request -- is + // posted, so the waiter never resumes inline from here. return std::noop_coroutine(); } diff --git a/test/unit/signal_set.cpp b/test/unit/signal_set.cpp index 278e031ad..2eb8009c0 100644 --- a/test/unit/signal_set.cpp +++ b/test/unit/signal_set.cpp @@ -771,6 +771,48 @@ struct signal_set_test } } + // A stop request made on another thread completes the parked wait, + // but the waiter resumes on a thread running the context, never + // inline on the thread that called request_stop(). + void testStopFromOtherThreadResumesOnContext() + { + io_context ioc(Backend); + signal_set s(ioc, SIGINT); + + std::stop_source src; + std::atomic parked{false}; + bool completed = false; + std::error_code result_ec = capy::error::eof; + std::thread::id resumed_on; + auto const run_thread = std::this_thread::get_id(); + + auto wait_task = [&]() -> capy::task<> { + parked.store(true, std::memory_order_release); + [[maybe_unused]] auto [ec, signum] = co_await s.wait(); + resumed_on = std::this_thread::get_id(); + result_ec = ec; + completed = true; + }; + capy::run_async(ioc.get_executor(), src.get_token())(wait_task()); + + std::thread stopper([&] { + while (!parked.load(std::memory_order_acquire)) + std::this_thread::yield(); + // Give the wait time to park so the stop takes the + // cancel-a-parked-wait path rather than the pre-park one. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + src.request_stop(); + }); + + // Bounded so a lost wakeup is a red assertion, not a hang. + std::ignore = ioc.run_for(std::chrono::seconds(5)); + stopper.join(); + + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + BOOST_TEST(resumed_on == run_thread); + } + void testShutdownWithPendingSignalSet() { // A set destroyed in the documented order gives its registrations @@ -1617,6 +1659,7 @@ struct signal_set_test testNormalDeliveryWithLiveToken(); testDestroyWithArmedStopToken(); testConcurrentStopRequestRace(); + testStopFromOtherThreadResumesOnContext(); testStopRacingTeardown(); testShutdownWithPendingSignalSet();