From 29b9126c262115c221e208a6de60f2e8d7c8d4b2 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Wed, 11 Oct 2023 22:02:52 +0200 Subject: [PATCH] Library: test coverage for lifecycle management Add a complete demonstration for a setup akin to what we use for the Session thread: a threaded component which manages itself but also exposes an external interface, which is opened/closed alongside --- src/lib/thread.hpp | 25 ++++++- src/steam/control/steam-dispatcher.cpp | 2 + .../library/meta/late-bind-instance-test.cpp | 3 +- .../library/thread-wrapper-lifecycle-test.cpp | 71 +++++++++++++------ wiki/thinkPad.ichthyo.mm | 45 +++++++----- 5 files changed, 100 insertions(+), 46 deletions(-) diff --git a/src/lib/thread.hpp b/src/lib/thread.hpp index 874eef85d..99c69cef1 100644 --- a/src/lib/thread.hpp +++ b/src/lib/thread.hpp @@ -225,6 +225,20 @@ namespace lib { void handle_begin_thread() { } ///< called immediately at start of thread void handle_after_thread() { } ///< called immediately before end of thread void handle_loose_thread() { } ///< called when destroying wrapper on still running thread + + /** + * allow to detach explicitly — independent from thread-function's state. + * @warning this function is borderline dangerous; it might be acceptable + * in a situation where the thread totally manages itself and the + * thread object is maintained in a unique_ptr. You must ensure that + * the thread function does not touch anything in the wrapper object + * after that point and only uses storage within its own scope. + */ + void detach_thread_from_wrapper() + { + if (isLive()) + threadImpl_.detach(); + } }; @@ -362,6 +376,7 @@ namespace lib { }; + /** * Policy-based configuration of thread lifecycle */ @@ -456,6 +471,13 @@ namespace lib { : launch{buildLauncher (forward(threadFunction), forward(args)...)} { } + template + Launch (RES (TAR::*memFun) (ARGS...), ARGS ...args) ///< ctor variant to bind a member function + : Launch{move (memFun) + ,lib::meta::InstancePlaceholder{} + ,forward (args)... } + { } + Launch&& threadID (string const& threadID) { @@ -737,8 +759,7 @@ namespace lib { }) .onOrphan([](thread::ThreadWrapper& wrapper) { - if (wrapper.isLive()) - wrapper.threadImpl_.detach(); + wrapper.detach_thread_from_wrapper(); })}; // Note: allocation tossed on the heap deliberately } // The thread-function will pick up and manage *this diff --git a/src/steam/control/steam-dispatcher.cpp b/src/steam/control/steam-dispatcher.cpp index ca7d9434e..143800139 100644 --- a/src/steam/control/steam-dispatcher.cpp +++ b/src/steam/control/steam-dispatcher.cpp @@ -231,6 +231,8 @@ namespace control { * any operation running in the Session thread is * started from here. When this loop terminates, * the »session subsystem« shuts down. + * @note the callback \a notifyEnd is typically bound + * to invoke SteamDispatcher::endRunningLoopState(). */ void runSessionThread (Subsys::SigTerm notifyEnd) diff --git a/tests/library/meta/late-bind-instance-test.cpp b/tests/library/meta/late-bind-instance-test.cpp index f9913ceb3..7da47cab8 100644 --- a/tests/library/meta/late-bind-instance-test.cpp +++ b/tests/library/meta/late-bind-instance-test.cpp @@ -28,11 +28,10 @@ #include "lib/test/run.hpp" #include "lib/meta/function.hpp" #include "lib/meta/tuple-helper.hpp" +#include "lib/test/test-helper.hpp" #include "lib/test/testdummy.hpp" #include "lib/format-cout.hpp" #include "lib/format-util.hpp" -#include "lib/test/diagnostic-output.hpp" -#include "lib/rational.hpp" #include "lib/util.hpp" #include diff --git a/tests/library/thread-wrapper-lifecycle-test.cpp b/tests/library/thread-wrapper-lifecycle-test.cpp index c9ea89534..f4c72ce4c 100644 --- a/tests/library/thread-wrapper-lifecycle-test.cpp +++ b/tests/library/thread-wrapper-lifecycle-test.cpp @@ -27,31 +27,27 @@ #include "lib/test/run.hpp" #include "lib/thread.hpp" -#include "lib/iter-explorer.hpp" -#include "lib/scoped-collection.hpp" -#include "lib/test/microbenchmark.hpp" -#include "lib/test/diagnostic-output.hpp" +#include "lib/test/testdummy.hpp" #include #include +#include using test::Test; using lib::explore; +using lib::test::Dummy; using std::atomic_uint; using std::this_thread::yield; using std::this_thread::sleep_for; using namespace std::chrono_literals; using std::chrono::system_clock; +using std::unique_ptr; namespace lib { namespace test{ - namespace { // test parameters - - const uint NUM_THREADS = 200; - const uint REPETITIONS = 10; - + namespace { using CLOCK_SCALE = std::micro; // Results are in µ-sec } @@ -96,11 +92,11 @@ namespace test{ double offset = Dur{threadStart - afterCtor}.count(); CHECK (offset > 0); } // Note: in practice we see here values > 100µs - // but in theory the thread might even overtake the launcher + // but in theory the thread might even overtake the launcher - /** - * @test attach user provided callback hooks to the thread lifecycle. + + /** @test attach user provided callback hooks to the thread lifecycle. */ void verifyThreadLifecycleHooks() @@ -124,29 +120,58 @@ namespace test{ /** - * @test verify a special setup to start a thread explicitly and to track - * the thread's lifecycle state. + * @test verify a special setup to start a thread explicitly + * and to track the thread's lifecycle state. + * - use a component to encapsulate the thread + * - this TestThread component is managed in a `unique_ptr` + * - thus it is explicitly possible to be _not_ in _running state_ + * - when starting the TestThread, a lifecycle callback is bound + * - at termination this callback will clear the unique_ptr + * - thus allocation and _running state_ is tied to the lifecycle */ void demonstrateExplicitThreadLifecycle() { - UNIMPLEMENTED ("demonstrate state change"); struct TestThread - : Thread + : ThreadHookable { - using Thread::Thread; + using ThreadHookable::ThreadHookable; - uint local{0}; + atomic_uint processVal{23}; void - doIt (uint a, uint b) ///< the actual operation running in a separate thread + doIt (uint haveFun) { - uint sum = a + b; -// sleep_for (microseconds{sum}); // Note: explicit random delay before local store - local = sum; + sleep_for (100us); + processVal = haveFun; + sleep_for (5ms); } }; - } + // Note the Dummy member allows to watch instance lifecycle + CHECK (0 == Dummy::checksum()); + + // the frontEnd allows to access the TestThread component + // and also represents the running state + unique_ptr frontEnd; + CHECK (not frontEnd); // obviously not running yet + + // start the thread and wire lifecycle callbacks + frontEnd.reset (new TestThread{ + TestThread::Launch{&TestThread::doIt, 55u} + .atExit([&]{ frontEnd.reset(); }) + .onOrphan([](thread::ThreadWrapper& wrapper) + { wrapper.detach_thread_from_wrapper(); }) + }); + + CHECK (frontEnd); // thread now marked as running + + CHECK (23 == frontEnd->processVal); // this value was set by the ctor in this thread + sleep_for (1ms); // wait for the thread function to become active + CHECK (55 == frontEnd->processVal); // changed by thread function + sleep_for (10ms); + + CHECK (not frontEnd); // meanwhile thread has finished + } // and also cleared the front-end from the `atExit`-hook }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index f48717de4..453e118b2 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -65009,9 +65009,9 @@ - + - + @@ -65045,7 +65045,7 @@ - + @@ -65115,7 +65115,7 @@ - + @@ -65157,7 +65157,7 @@ - + @@ -65211,7 +65211,7 @@ - + @@ -65268,7 +65268,7 @@ - + @@ -65328,7 +65328,7 @@ - + @@ -65348,7 +65348,7 @@ - + @@ -65475,7 +65475,7 @@ - + @@ -65489,7 +65489,7 @@ - + @@ -65546,7 +65546,7 @@ - + @@ -65808,7 +65808,7 @@ - + @@ -66208,7 +66208,7 @@ - + @@ -66224,7 +66224,7 @@ - + @@ -66235,7 +66235,7 @@ - + @@ -66303,7 +66303,8 @@ - + + @@ -66322,13 +66323,19 @@ - - + + + + + + + +