From 0a38296a5031da1b33e0586aa09b6c450a425e46 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Mon, 19 Oct 2026 18:40:57 +0200 Subject: [PATCH] refactor: give up all work in shutdown() and destroy services once A context's destructor repeated destroy() and its drain until the drain found nothing, so a service could post from its destructor at any depth and the context still cleaned up after it. Services now give up all their work in shutdown(), joining any threads they own there, and do not post from their destructors. thread_pool and blocking_context drain until the queue is empty, which also takes the work of any service an unwound chain creates, since it is shut down as it is created. They then destroy the services once, and drain one last time for stray work a destructor posts anyway. No service may be created after destroy() returns: ~execution_context would destroy it after the derived context's members it may still reach. Debug builds now check this. The service documentation said shutdown() must not block; it now allows joining the service's own threads. The execution_context documentation, the custom executor guide and the two executor examples describe the same order. --- .../pages/8.examples/8n.custom-executor.adoc | 2 +- example/custom-executor/custom_executor.cpp | 14 +- example/gui-integration/gui_integration.cpp | 15 +- include/boost/capy/ex/execution_context.hpp | 42 ++++-- include/boost/capy/ex/thread_pool.hpp | 4 +- src/ex/execution_context.cpp | 5 + src/ex/thread_pool.cpp | 28 ++-- src/test/run_blocking.cpp | 24 ++-- test/unit/death_test.hpp | 10 ++ test/unit/ex/execution_context.cpp | 26 ++++ test/unit/ex/run_blocking.cpp | 92 +++++++++---- test/unit/ex/strand.cpp | 39 ++++++ test/unit/ex/thread_pool.cpp | 130 ++++++++++++++---- 13 files changed, 315 insertions(+), 116 deletions(-) diff --git a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc index 719772f61..76b0be567 100644 --- a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc +++ b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc @@ -33,7 +33,7 @@ target_link_libraries(custom_executor PRIVATE Boost::capy) include::example$custom-executor/custom_executor.cpp[tag=inherit] ---- -The destructor must call `shutdown()`, give up any work still queued with cpp:abandon[], then call `destroy()`. Destroying a service can post more work, and unwinding an abandoned chain can create a service, so the destructor repeats `destroy()` and the drain until a drain finds nothing. The queue holds copies of each cpp:continuation[], so the owner that cpp:abandon[] needs travels with the handle. +The destructor must call `shutdown()`, give up any work still queued with cpp:abandon[], then call `destroy()`. The drain also takes work posted while it runs. That includes the work of a service an unwound chain creates, which is shut down as it is created. Services give up their work in `shutdown()`, so destroying them posts nothing. The destructor drains once more for stray work a destructor posts anyway. No service may be created after `destroy()`. The queue holds copies of each cpp:continuation[], so the owner that cpp:abandon[] needs travels with the handle. === The Executor Concept diff --git a/example/custom-executor/custom_executor.cpp b/example/custom-executor/custom_executor.cpp index 957bcdfc8..af5a1247c 100644 --- a/example/custom-executor/custom_executor.cpp +++ b/example/custom-executor/custom_executor.cpp @@ -52,25 +52,19 @@ class run_loop : public capy::execution_context // are unwound. An unwind posted here is given up in turn. auto drain = [this] { - bool any = false; while (!queue_.empty()) { auto c = queue_.front(); queue_.pop(); - any = true; capy::abandon(c); } - return any; }; shutdown(); drain(); - // A service's destructor may post too: repeat until nothing - // is left of either. - do - { - destroy(); - } - while (drain()); + destroy(); + // Services give up their work in shutdown(), but anything a + // destructor posts anyway is still given up. + drain(); } run_loop(run_loop const&) = delete; diff --git a/example/gui-integration/gui_integration.cpp b/example/gui-integration/gui_integration.cpp index bb5cc1f3e..d52fed003 100644 --- a/example/gui-integration/gui_integration.cpp +++ b/example/gui-integration/gui_integration.cpp @@ -89,30 +89,25 @@ class gui_app : public capy::execution_context // are unwound. An unwind posted here is given up in turn. auto drain = [this] { - bool any = false; for(;;) { capy::continuation c; { std::lock_guard lock(m_); if(queue_.empty()) - return any; + return; c = queue_.front(); queue_.pop_front(); } - any = true; capy::abandon(c); } }; shutdown(); drain(); - // A service's destructor may post too: repeat until nothing - // is left of either. - do - { - destroy(); - } - while(drain()); + destroy(); + // Services give up their work in shutdown(), but anything a + // destructor posts anyway is still given up. + drain(); } gui_app(gui_app const&) = delete; diff --git a/include/boost/capy/ex/execution_context.hpp b/include/boost/capy/ex/execution_context.hpp index b36dc78c0..3cca6cea3 100644 --- a/include/boost/capy/ex/execution_context.hpp +++ b/include/boost/capy/ex/execution_context.hpp @@ -38,10 +38,12 @@ namespace capy { execution facilities like event loops and thread pools. Derived class destructors must call `shutdown()` and `destroy()` to ensure proper service cleanup before member destruction. Between the two, - a context that queues work abandons what is still queued. Since a - service's destructor can post and an unwound chain can create a - service, it then repeats `destroy()` and that drain until the - drain finds nothing. + a context that queues work abandons what is still queued, including + work posted while it does so. A service created by an unwound chain + is shut down as it is created, so its work is taken too. Services + do not post from their destructors. A context may drain once more + after `destroy()`, so that stray work is still destroyed, but no + service may be created after `destroy()` returns. @par Service Lifecycle Services are created on first use via `use_service()` or explicitly @@ -106,8 +108,14 @@ class BOOST_CAPY_DECL Derived classes must implement the pure virtual `shutdown()` member function, which is called when the owning execution context is - being destroyed. The `shutdown()` function should release resources - and cancel outstanding operations without blocking. + being destroyed. The `shutdown()` function gives up all of the + service's work, releases resources and cancels outstanding + operations. It does not block, except to wait for work the + service already started, such as its own threads or operations + it submitted to the system, so that nothing that work posts + arrives later. The + destructor must not post: by the time it runs, the context has + already drained its queue. @par Deriving from service @li Implement `shutdown()` to perform cleanup. @@ -133,10 +141,13 @@ class BOOST_CAPY_DECL /** Called when the owning execution context shuts down. - Implementations should release resources and cancel any - outstanding asynchronous operations. This function must - not block and must not throw exceptions. Services are - shut down in reverse order of creation. + Implementations give up all of the service's work here, + release resources and cancel any outstanding asynchronous + operations, rather than in the destructor. This function + must not throw exceptions, and must not block except to + wait for work the service already started, such as threads + it owns. Services are shut down in + reverse order of creation. @par Exception Safety No-throw guarantee. @@ -472,8 +483,9 @@ class BOOST_CAPY_DECL /** Destroy all services. Deletes all services in reverse order of creation. Derived - classes should call this as the final step of destruction. - This function is idempotent; subsequent calls have no effect. + classes call this after giving up the work still queued, so + that work is destroyed while every service exists. This + function is idempotent; subsequent calls have no effect. @par Preconditions @li `shutdown()` was called. @@ -496,6 +508,11 @@ class BOOST_CAPY_DECL @par Thread Safety Not thread-safe. Must not be called concurrently with other operations on this execution_context. + + @note No service may be created after this returns. It would + outlive the derived context's members, which its destructor + may still reach. Debug builds check this when the context is + destroyed. */ void destroy() noexcept; @@ -536,6 +553,7 @@ class BOOST_CAPY_DECL std::pmr::memory_resource* frame_alloc_ = nullptr; service* head_ = nullptr; bool shutdown_ = false; + bool destroyed_ = false; }; template< typename Derived > diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp index e5a905caf..4dc84b317 100644 --- a/include/boost/capy/ex/thread_pool.hpp +++ b/include/boost/capy/ex/thread_pool.hpp @@ -61,7 +61,9 @@ class BOOST_CAPY_DECL (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. + the pool is destroyed, so it has completed first. A thread + owned by one of this pool's services may post or dispatch + until that service's `shutdown()` returns. */ ~thread_pool(); diff --git a/src/ex/execution_context.cpp b/src/ex/execution_context.cpp index 2a67b8fe8..bed8f02d2 100644 --- a/src/ex/execution_context.cpp +++ b/src/ex/execution_context.cpp @@ -23,6 +23,10 @@ execution_context() execution_context:: ~execution_context() { + // A service created after a derived destructor destroyed the + // services would be destroyed here, after the derived members it + // may still reach. + BOOST_CAPY_ASSERT(!destroyed_ || !head_); shutdown(); destroy(); } @@ -54,6 +58,7 @@ destroy() noexcept head_ = p->next_; delete p; } + destroyed_ = true; } execution_context::service* diff --git a/src/ex/thread_pool.cpp b/src/ex/thread_pool.cpp index 68cdf1df7..4528c7d19 100644 --- a/src/ex/thread_pool.cpp +++ b/src/ex/thread_pool.cpp @@ -115,16 +115,12 @@ class thread_pool::impl return current_.get() == this; } - // Abandon queued coroutines. Called after the services shut down, - // since their shutdown can post more, and again after they are - // destroyed, for anything their destructors post. Each is taken - // under the lock, as other threads may still post, and abandoned - // outside it, as abandoning can post. Returns whether there was - // anything to abandon. - bool + // Abandon queued coroutines. Each is taken under the lock, as other + // threads may still post, and abandoned outside it, as abandoning + // can post. + void drain_abandoned() noexcept { - bool any = false; for(;;) { continuation* c; @@ -133,8 +129,7 @@ class thread_pool::impl c = pop(); } if(!c) - return any; - any = true; + return; if(c->h && c->h != std::noop_coroutine()) capy::abandon(*c); } @@ -302,14 +297,13 @@ thread_pool:: impl_->stop(); impl_->join(); shutdown(); + // Takes what the unwinds post too, and the work of any service they + // create, which is shut down as it is created. + impl_->drain_abandoned(); + destroy(); + // Services give up their work in shutdown(), but anything a + // destructor posts anyway is still destroyed. impl_->drain_abandoned(); - // A frame the drain unwinds can create a service, whose destructor - // can post again: repeat until nothing is left of either. - do - { - destroy(); - } - while(impl_->drain_abandoned()); delete impl_; } diff --git a/src/test/run_blocking.cpp b/src/test/run_blocking.cpp index fda50d140..88d03713a 100644 --- a/src/test/run_blocking.cpp +++ b/src/test/run_blocking.cpp @@ -46,36 +46,32 @@ blocking_context::blocking_context() blocking_context::~blocking_context() { - // Nothing will run these now; abandon them as their holder. The - // services may post while they shut down or are destroyed, so the - // queue outlives both. Each is taken under the lock, as other - // threads may still post, and abandoned outside it. + // Nothing will run these now; abandon them as their holder. Each is + // taken under the lock and abandoned outside it, since abandoning + // one may post another. auto drain = [this] { - bool any = false; for(;;) { continuation c; { std::lock_guard lock(impl_->mtx); if(impl_->queue.empty()) - return any; + return; c = impl_->queue.front(); impl_->queue.pop(); } - any = true; capy::abandon(c); } }; shutdown(); + // Takes what the unwinds post too, and the work of any service they + // create, which is shut down as it is created. + drain(); + destroy(); + // Services give up their work in shutdown(), but anything a + // destructor posts anyway is still destroyed. drain(); - // A frame the drain unwinds can create a service, whose destructor - // can post again: repeat until nothing is left of either. - do - { - destroy(); - } - while(drain()); delete impl_; } diff --git a/test/unit/death_test.hpp b/test/unit/death_test.hpp index bbb4a2244..c59ea3bbd 100644 --- a/test/unit/death_test.hpp +++ b/test/unit/death_test.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -70,6 +71,15 @@ terminates(F fn) return WIFEXITED(status) && WEXITSTATUS(status) == terminated_status; } +/// Return true when `fn` aborts, as a failed assertion does. +template +bool +aborts(F fn) +{ + int status = run_in_child(fn); + return WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT; +} + /// Return true when `fn` returns, and returns true if it returns a bool. template bool diff --git a/test/unit/ex/execution_context.cpp b/test/unit/ex/execution_context.cpp index 040ff74b6..adeef0ab4 100644 --- a/test/unit/ex/execution_context.cpp +++ b/test/unit/ex/execution_context.cpp @@ -11,6 +11,7 @@ #include #include +#include "death_test.hpp" #include "test_helpers.hpp" #include @@ -519,6 +520,28 @@ struct execution_context_test BOOST_TEST_NE(new_mr, default_mr); } +#if !defined(_WIN32) && !defined(NDEBUG) + // Once a context has destroyed its services, a service created + // later would outlive the derived context's members, so a debug + // build stops the program. + void + testServiceCreatedAfterDestroyAsserts() + { + struct late_creating_context : execution_context + { + ~late_creating_context() + { + shutdown(); + destroy(); + make_service(); + } + }; + BOOST_TEST(aborts([] { + late_creating_context ctx; + })); + } +#endif + void run() { @@ -542,6 +565,9 @@ struct execution_context_test testGetFrameAllocator(); testSetFrameAllocatorRawPointer(); testSetFrameAllocatorTemplate(); +#if !defined(_WIN32) && !defined(NDEBUG) + testServiceCreatedAfterDestroyAsserts(); +#endif } }; diff --git a/test/unit/ex/run_blocking.cpp b/test/unit/ex/run_blocking.cpp index b72975ba7..3aee026d3 100644 --- a/test/unit/ex/run_blocking.cpp +++ b/test/unit/ex/run_blocking.cpp @@ -105,13 +105,12 @@ struct dtor_poster : execution_context::service void shutdown() override {} }; -// Starts posting from another thread when the context's services shut -// down, while the context drains, and joins that thread when destroyed. +// Posts from a thread of its own while the context's services shut down, +// and joins that thread before its shutdown() returns. struct shutdown_poster : execution_context::service { blocking_executor ex_; std::vector* cs_; - std::thread t_; shutdown_poster( execution_context&, @@ -122,18 +121,14 @@ struct shutdown_poster : execution_context::service { } - ~shutdown_poster() - { - t_.join(); - } - void shutdown() override { - t_ = std::thread([this] + std::thread t([this] { for(auto& c : *cs_) ex_.post(c); }); + t.join(); } }; @@ -158,13 +153,54 @@ struct shutdown_dispatcher : execution_context::service } }; -// A second dtor_poster, so one can be created while the first exists. -struct late_dtor_poster : dtor_poster +// Posts when the context's services shut down, as a service giving up +// its work would. +struct late_poster : execution_context::service { - using dtor_poster::dtor_poster; + blocking_executor ex_; + continuation* c_; + + late_poster(execution_context&, blocking_executor ex, continuation& c) + : ex_(ex) + , c_(&c) + { + } + + void shutdown() override + { + ex_.post(*c_); + } +}; + +// A second late_poster, so one can be created while the first exists. +struct later_poster : late_poster +{ + using late_poster::late_poster; +}; + +// Records, when the context destroys it, how many probes were already +// destroyed. Made after the services the test creates, it is destroyed +// before them. +struct teardown_witness : execution_context::service +{ + int const* down_; + int* seen_; + + teardown_witness(execution_context&, int const& down, int& seen) + : down_(&down) + , seen_(&seen) + { + } + + ~teardown_witness() + { + *seen_ = *down_; + } + + void shutdown() override {} }; -// Creates a late_dtor_poster on the context when destroyed. +// Creates a later_poster on the context when destroyed. struct poster_maker { blocking_context* ctx; @@ -185,7 +221,7 @@ struct poster_maker ~poster_maker() { if(ctx) - ctx->make_service(ctx->get_executor(), *c); + ctx->make_service(ctx->get_executor(), *c); } }; @@ -470,12 +506,12 @@ struct run_blocking_test BOOST_TEST_EQ(down, 1); } - // Work posted from another thread while the context drains is - // neither lost nor torn from the queue. + // Work a service's own thread posts while the service shuts down is + // destroyed with the context, none of it lost. void - testPostDuringDrain() + testPostFromServiceThreadAtShutdown() { - constexpr int n = 2000; + constexpr int n = 16; int down = 0; auto probe = [](destroy_count_t) -> task { co_return; }; std::vector cs; @@ -541,7 +577,8 @@ struct run_blocking_test BOOST_TEST_EQ(down, 1); } - // Work posted while the services are destroyed is destroyed too. + // A service should not post from its destructor, but work one posts + // anyway is still destroyed with the context. void testPostFromServiceDestructor() { @@ -556,12 +593,15 @@ struct run_blocking_test BOOST_TEST_EQ(down, 1); } - // A frame destroyed by the context's last drain may create a service - // whose destructor posts again; that work is destroyed too. + // A frame destroyed while the context shuts down may create a service. + // That service is shut down as it is created, and the drain destroys + // the work it gives up before the services made before the teardown + // are destroyed. void - testServiceCreatedByLastDrainDestroyed() + testServiceCreatedDuringDrain() { int down = 0; + int seen = -1; auto maker = [](poster_maker) -> task { co_return; }; auto t = plain_probe(destroy_count_t(down)); continuation probe_c{t.handle()}; @@ -572,8 +612,10 @@ struct run_blocking_test auto m = maker(poster_maker(ctx, probe_c)); maker_c.h = m.handle(); m.release(); - ctx.make_service(ctx.get_executor(), maker_c); + ctx.make_service(ctx.get_executor(), maker_c); + ctx.make_service(down, seen); } + BOOST_TEST_EQ(seen, 1); BOOST_TEST_EQ(down, 1); } @@ -643,10 +685,10 @@ struct run_blocking_test testDestroyedContextDestroysQueued(); testDispatchDuringShutdown(); testAbandonedChainThrows(); - testPostDuringDrain(); + testPostFromServiceThreadAtShutdown(); testPostDuringServiceShutdown(); testPostFromServiceDestructor(); - testServiceCreatedByLastDrainDestroyed(); + testServiceCreatedDuringDrain(); } }; diff --git a/test/unit/ex/strand.cpp b/test/unit/ex/strand.cpp index e81381195..b0e3d0cab 100644 --- a/test/unit/ex/strand.cpp +++ b/test/unit/ex/strand.cpp @@ -1067,6 +1067,44 @@ struct strand_test BOOST_TEST_EQ(d, 1); } + // The strand service gives up every strand's queued work in its + // shutdown(), so a strand dropped afterwards has nothing left to + // give up, and destroying the services posts nothing. + void + testShutdownLeavesNothingForStrandDestructor() + { + int down[2] = {}; + continuation conts[2] = {}; + std::queue q; + auto probe = [](destroy_count_t) -> task { co_return; }; + for(std::size_t i = 0; i < 2; ++i) + { + auto t = probe(destroy_count_t(down[i])); + conts[i].h = t.handle(); + t.release(); + } + auto ctx = std::make_unique(); + queuing_executor qe(q); + qe.ctx_ = ctx.get(); + auto s = std::make_unique>(qe); + s->post(conts[0]); + s->post(conts[1]); + BOOST_TEST_EQ(q.size(), 1u); + capy::abandon(q.front()); + q.pop(); + for(int d : down) + BOOST_TEST_EQ(d, 0); + + ctx->shutdown(); + for(int d : down) + BOOST_TEST_EQ(d, 1); + s.reset(); + ctx.reset(); + for(int d : down) + BOOST_TEST_EQ(d, 1); + BOOST_TEST(q.empty()); + } + void testShutdownReleasesLiveStrand() { @@ -1571,6 +1609,7 @@ struct strand_test run() { testShutdownDestroysQueuedAcrossStrands(); + testShutdownLeavesNothingForStrandDestructor(); testShutdownDestroysQueuedParentedChain(); testShutdownReleasesLiveStrand(); testConstruct(); diff --git a/test/unit/ex/thread_pool.cpp b/test/unit/ex/thread_pool.cpp index 1a9b4659e..8c573f681 100644 --- a/test/unit/ex/thread_pool.cpp +++ b/test/unit/ex/thread_pool.cpp @@ -18,6 +18,7 @@ #include #include +#include "death_test.hpp" #include "test_helpers.hpp" #include @@ -189,13 +190,12 @@ struct dtor_poster : execution_context::service void shutdown() override {} }; -// Starts posting from another thread when the pool's services shut -// down, while the pool drains, and joins that thread when destroyed. +// Posts from a thread of its own while the pool's services shut down, +// and joins that thread before its shutdown() returns. struct shutdown_poster : execution_context::service { thread_pool::executor_type ex_; std::vector* cs_; - std::thread t_; shutdown_poster( execution_context&, @@ -206,28 +206,46 @@ struct shutdown_poster : execution_context::service { } - ~shutdown_poster() - { - t_.join(); - } - void shutdown() override { - t_ = std::thread([this] + std::thread t([this] { for(auto& c : *cs_) ex_.post(c); }); + t.join(); } }; -// A second dtor_poster, so one can be created while the first exists. -struct late_dtor_poster : dtor_poster +// A second late_poster, so one can be created while the first exists. +struct later_poster : late_poster { - using dtor_poster::dtor_poster; + using late_poster::late_poster; }; -// Creates a late_dtor_poster on the pool when destroyed. +// Records, when the pool destroys it, how many probes were already +// destroyed. Made after the services the test creates, it is destroyed +// before them. +struct teardown_witness : execution_context::service +{ + int const* down_; + int* seen_; + + teardown_witness(execution_context&, int const& down, int& seen) + : down_(&down) + , seen_(&seen) + { + } + + ~teardown_witness() + { + *seen_ = *down_; + } + + void shutdown() override {} +}; + +// Creates a later_poster on the pool when destroyed. struct poster_maker { thread_pool* pool; @@ -248,10 +266,37 @@ struct poster_maker ~poster_maker() { if(pool) - pool->make_service(pool->get_executor(), *c); + pool->make_service(pool->get_executor(), *c); } }; +#if !defined(_WIN32) && !defined(NDEBUG) +// Creates a dtor_poster on the pool when destroyed. +struct dtor_poster_maker +{ + thread_pool* pool; + continuation* c; + + dtor_poster_maker(thread_pool& p, continuation& cont) noexcept + : pool(&p) + , c(&cont) + { + } + + dtor_poster_maker(dtor_poster_maker&& o) noexcept + : pool(std::exchange(o.pool, nullptr)) + , c(o.c) + { + } + + ~dtor_poster_maker() + { + if(pool) + pool->make_service(pool->get_executor(), *c); + } +}; +#endif + // Parks the awaiting coroutine in a slot and signals that it did. struct signal_park { @@ -1026,12 +1071,15 @@ struct thread_pool_test BOOST_TEST(st.balanced()); } - // A frame destroyed by the pool's last drain may create a service - // whose destructor posts again; that work is destroyed too. + // A frame destroyed while the pool shuts down may create a service. + // That service is shut down as it is created, and the drain destroys + // the work it gives up before the services made before the teardown + // are destroyed. void - testServiceCreatedByLastDrainDestroyed() + testServiceCreatedDuringDrain() { int down = 0; + int seen = -1; auto probe = [](destroy_count_t) -> task { co_return; }; auto maker = [](poster_maker) -> task { co_return; }; auto p = probe(destroy_count_t(down)); @@ -1043,11 +1091,38 @@ struct thread_pool_test auto m = maker(poster_maker(pool, probe_c)); maker_c.h = m.handle(); m.release(); - pool.make_service(pool.get_executor(), maker_c); + pool.make_service(pool.get_executor(), maker_c); + pool.make_service(down, seen); } + BOOST_TEST_EQ(seen, 1); BOOST_TEST_EQ(down, 1); } +#if !defined(_WIN32) && !defined(NDEBUG) + // A service whose destructor posts breaks the contract, and the work + // it posts is still destroyed. If destroying that work creates a + // service whose destructor posts too, that service would outlive the + // pool's queue, so a debug build stops the program instead. + void + testServiceCreatedAfterDestroyAsserts() + { + BOOST_TEST(aborts([] { + auto probe = [](destroy_count_t) -> task { co_return; }; + auto maker = [](dtor_poster_maker) -> task { co_return; }; + int down = 0; + auto p = probe(destroy_count_t(down)); + continuation probe_c{p.handle()}; + p.release(); + continuation maker_c; + thread_pool pool(1); + auto m = maker(dtor_poster_maker(pool, probe_c)); + maker_c.h = m.handle(); + m.release(); + pool.make_service(pool.get_executor(), maker_c); + })); + } +#endif + // A started chain whose leaf is queued when the pool is destroyed // comes down there, on another thread than its child was made on, // with every frame freed. @@ -1103,8 +1178,8 @@ struct thread_pool_test BOOST_TEST_EQ(down, 1); } - // Work posted while the pool's services are destroyed is destroyed - // with the pool. + // A service should not post from its destructor, but work one posts + // anyway is still destroyed with the pool. void testPostFromServiceDestructorDestroyed() { @@ -1120,12 +1195,12 @@ struct thread_pool_test BOOST_TEST_EQ(down, 1); } - // Work posted from another thread while the pool drains is neither - // lost nor torn from the queue. + // Work a service's own thread posts while the service shuts down is + // destroyed with the pool, none of it lost. void - testPostDuringDrain() + testPostFromServiceThreadAtShutdown() { - constexpr int n = 2000; + constexpr int n = 16; int down = 0; auto probe = [](destroy_count_t) -> task { co_return; }; std::vector cs; @@ -1175,9 +1250,12 @@ struct thread_pool_test testForeignPostAfterJoin(); testPostDuringShutdownDestroyed(); testPostFromServiceDestructorDestroyed(); - testPostDuringDrain(); + testPostFromServiceThreadAtShutdown(); testDestroyDrainsParentedChain(); - testServiceCreatedByLastDrainDestroyed(); + testServiceCreatedDuringDrain(); +#if !defined(_WIN32) && !defined(NDEBUG) + testServiceCreatedAfterDestroyAsserts(); +#endif testTeardownFromAnotherThreadWithLiveRoot(); testJoinRacesFirstPost(); testJoinWaitingThenFirstPost();