diff --git a/src/lib/thread.hpp b/src/lib/thread.hpp index 6418ef387..a356ecf08 100644 --- a/src/lib/thread.hpp +++ b/src/lib/thread.hpp @@ -60,6 +60,10 @@ ** caller. Such threads *must* be joined however, and thus the destructor immediately ** terminates the application in case the thread is still running. ** + ** A further variant ThreadHookable allows to attach user-provided callbacks invoked from + ** the thread lifecycle; this can be used to build a thread-object that manages itself + ** autonomously, or a thread that opens / closes interfaces tied to its lifecycle. + ** ** ## Synchronisation ** The C++ standard provides that the end of the `std::thread` constructor _syncs-with_ the ** start of the new thread function, and likewise the end of the thread activity _syncs-with_ @@ -148,6 +152,7 @@ namespace lib { using lib::meta::typeSymbol; using lib::meta::_Fun; + using util::isnil; using std::function; using std::forward; using std::move; @@ -180,7 +185,7 @@ namespace lib { { } ThreadWrapper (string const& threadID) - : threadID_{util::sanitise (threadID)} + : threadID_{isnil(threadID)? "sub-thread" : util::sanitise (threadID)} , threadImpl_{} //Note: deliberately not starting the thread yet... { } @@ -264,38 +269,9 @@ namespace lib { /** * Thread Lifecycle Policy Extension: - * additionally self-manage the thread-wrapper allocation. - * @warning the thread-wrapper must have been heap-allocated. - */ - template - struct PolicySelfManaged - : PolicyLaunchOnly - { - using BasePol = PolicyLaunchOnly; - using BasePol::BasePol; - - void - handle_after_thread() - { - TAR* selfAllocation = static_cast( - static_cast (this)); - if (BAS::isLive()) - BAS::threadImpl_.detach(); - delete selfAllocation; - } - - void - handle_loose_thread() - { - ALERT (thread, "Self-managed thread was deleted from outside. Abort."); - } - }; - - - /** - * Thread Lifecycle Policy Extension: - * additionally self-manage the thread-wrapper allocation. - * @warning the thread-wrapper must have been heap-allocated. + * invoke user-provided callbacks from within thread lifecycle. + * @see ThreadLifecycle::invokeThreadFunction for invocation... + * @see ThreadLifecycle::Launch for configuration of these hooks. */ template struct PolicyLifecycleHook @@ -501,7 +477,7 @@ namespace lib { * @tparam FUN type of the function maintained in #PolicyLifecycleHook * @note the user provided functor can take any type as argument, which * is reachable by static cast from the thread-wrapper. Especially - * this allows both for low-level and userclass-internal hooks. + * this allows both for low-level and userclass-internal hooks. */ template auto @@ -539,7 +515,7 @@ namespace lib { }); } - /** generic helper to add another »onion layer« to this config builder */ + /** generic helper to add another »onion layer« to this config builder */ Launch&& addLayer (Act action) { @@ -617,6 +593,10 @@ namespace lib { + + + + /************************************************************************//** * A thin convenience wrapper to simplify thread-handling. * The implementation is backed by the C++ standard library. @@ -631,7 +611,6 @@ namespace lib { class Thread : public thread::ThreadLifecycle { - public: using ThreadLifecycle::ThreadLifecycle; @@ -663,7 +642,6 @@ namespace lib { : public thread::ThreadLifecycle { using Impl = thread::ThreadLifecycle; - public: using Impl::Impl; @@ -717,34 +695,54 @@ namespace lib { - /************************************************************************//** - * Special configuration for a »fire-and-forget«-Thread. - * @internal this class is meant for subclassing. Start with #launchDetached() - * @tparam TAR the concrete type of the subclass to be started as autonomous, - * self-managed thread. Must be passed down since thread deletes itself. + /** + * Launch an autonomous self-managing thread (and forget about it). + * The thread-object is allocated to the heap and will delete itself on termination. + * @tparam TAR concrete type of the subclass to be started as autonomous detached thread. + * @param launchBuilder a flexible thread launch configuration to be used for starting; + * especially this contains the thread function and arguments to run in the thread. + * @note hooks `atExit` and `onOrphan` defined in the \a launchBuilder will be overridden. + * @remarks the `atExit` hook is the last piece of code executed within the thread; here it + * takes a pointer to the allocated instance of type \a TAR and deletes it from heap. + * Obviously this will invoke the destructor of the thread-wrapper eventually, at which + * point the `onOrphan` hook is invoked (since the thread is still running); at this point + * the C++ thread-handle is detached, so that invoking the dtor of std::thread will not + * terminate the system, rather just let the thread go away. The net result is an object + * of type \a TAR placed on the heap and kept there precisely as long as the thread runs. */ - class ThreadAutonomous - : public thread::ThreadLifecycle - { - using Impl = thread::ThreadLifecycle; - public: - using Impl::Impl; - }; + template + inline void + launchDetached (ThreadHookable::Launch&& launchBuilder) + { + static_assert (lib::meta::is_Subclass()); + + new TAR{move(launchBuilder) + .atExit([](TAR& selfAllocation) + { + delete &selfAllocation; + }) + .onOrphan([](thread::ThreadWrapper& wrapper) + { + if (wrapper.isLive()) + wrapper.threadImpl_.detach(); + })}; + // Note: allocation tossed on the heap deliberately + } // The thread-function will pick up and manage *this /** * Launch an autonomous self-managing thread (and forget about it). - * The thread-wrapper is allocated to the heap and will delete itself on termination. * @tparam TAR concrete type of the subclass to be started as autonomous detached thread. * @param args a valid argument list to call the ctor of thread::ThreadLifecycle */ - template + template inline void - launchDetached (INVO&& ...args) + launchDetached (string const& threadID, INVO&& ...args) { - new ThreadAutonomous{forward (args)...}; // Thread will pick up and manage *this + using Launch = typename TAR::Launch; + launchDetached (Launch{forward (args)...} + .threadID (threadID)); } - } // namespace lib #endif /*LIB_THREAD_H*/ diff --git a/tests/library/thread-wrapper-lifecycle-test.cpp b/tests/library/thread-wrapper-lifecycle-test.cpp index b6be52b3e..c9ea89534 100644 --- a/tests/library/thread-wrapper-lifecycle-test.cpp +++ b/tests/library/thread-wrapper-lifecycle-test.cpp @@ -82,31 +82,34 @@ namespace test{ using Point = system_clock::time_point; Point threadStart; Point afterCtor; + + // the new thread starts immediately from ctor-call... Thread thread("lifecycle", [&]{ threadStart = system_clock::now(); }); afterCtor = system_clock::now(); + CHECK (thread); // thread marked as running + while (thread) yield(); + CHECK (not thread); // thread now marked as detached/dead double offset = Dur{threadStart - afterCtor}.count(); -SHOW_EXPR(offset) CHECK (offset > 0); - } + } // Note: in practice we see here values > 100µs + // but in theory the thread might even overtake the launcher /** - * @test verify a special setup to start a thread explicitly and to track - * the thread's lifecycle state. + * @test attach user provided callback hooks to the thread lifecycle. */ void verifyThreadLifecycleHooks() { - atomic_uint stage{0}; + atomic_uint stage{0}; // flexible launch-builder syntax: ThreadHookable thread{ThreadHookable::Launch([]{ sleep_for (5ms); }) - .threadID("hooked thread") .atStart([&]{ stage = 1; }) - .atExit ([&]{ stage = 2; })}; - + .atExit ([&]{ stage = 2; }) + .threadID("hooked thread")}; CHECK (thread); CHECK (0 == stage); diff --git a/tests/vault/gear/work-force-test.cpp b/tests/vault/gear/work-force-test.cpp index 4f8debfb2..da61929c9 100644 --- a/tests/vault/gear/work-force-test.cpp +++ b/tests/vault/gear/work-force-test.cpp @@ -388,12 +388,12 @@ namespace test { auto fullCnt = work::Config::COMPUTATION_CAPACITY; wof.activate (1.0); - sleep_for(1ms); + sleep_for(2ms); CHECK (fullCnt == uniqueCnt); CHECK (fullCnt == wof.size()); wof.activate (2.0); - sleep_for(1ms); + sleep_for(2ms); CHECK (2*fullCnt == uniqueCnt); CHECK (2*fullCnt == wof.size()); @@ -401,11 +401,11 @@ namespace test { CHECK (0 == wof.size()); uniqueCnt.clear(); - sleep_for(1ms); + sleep_for(2ms); CHECK (0 == uniqueCnt); wof.activate (0.5); - sleep_for(1ms); + sleep_for(2ms); CHECK (fullCnt/2 == uniqueCnt); CHECK (fullCnt/2 == wof.size()); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 6d5ed6361..44534c55b 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -65012,7 +65012,7 @@ - + @@ -65038,9 +65038,9 @@ - - - + + + @@ -65115,7 +65115,7 @@ - + @@ -65131,8 +65131,9 @@ - - + + + @@ -65184,7 +65185,8 @@ - + + @@ -65251,9 +65253,7 @@ - - - +

und zwar, sofern oben in der konkreten Subklasse irgendwo ein mix-in verwendet wird — in dem Fall müßte nämlich der Pointer nachjustiert werden @@ -65267,17 +65267,13 @@ - - - - + + - - - +

ohne Subclassing ist die ganze Übung etwas überzogen @@ -65285,9 +65281,7 @@ - - - +

...es ist ja nett, daß der Thread-Wrapper sich selbst verwaltet — und der ganze Aufwand bloß für zwei interne Datenfelder?  wäre da nicht ein »workaround« angemessener? @@ -65297,9 +65291,7 @@ - - - +

wozu dann überhaupt noch das Front-End? Um einen etwas sonderbaren new-Aufruf einzupacken? @@ -65307,15 +65299,17 @@ - + + - + - + + @@ -65324,9 +65318,7 @@ - - - +

...und wir wären aus der Nummer raus: das ist nämlich oberhalb der protected inheritance @@ -65336,6 +65328,24 @@ + + + + +

+ das ist die viel elegantere Lösung: Variante-1 durch Variante-2  implemenitert +

+ +
+ + + + + + + + + @@ -65416,9 +65426,7 @@ - - - +

ganz elegant erst mal auf TheadLifecycle downcasten @@ -65426,9 +65434,7 @@ - - - +

was „rein zufällig“ hier in diesem nested scope in ThreadLifecycle erlaubt und auch sicher ist, denn hier haben wir noch Zugriff auf die protected geerbten Policy-Klassen... @@ -65440,7 +65446,8 @@ - + + @@ -65465,15 +65472,13 @@ - + - - - +

...sonst wäre das ganze Unterfangen theoretischer Natur; nur dadurch wird es interessant, Storage zu verwalten und einen Zugang freizuschalten @@ -65494,7 +65499,7 @@ - + @@ -65509,9 +65514,7 @@ - - - +

⟹ Initialisierung von Policy-Bausteinen ist nicht möglich @@ -65521,9 +65524,7 @@ - - - +

(seufz) @@ -65539,9 +65540,9 @@ - + - + @@ -65553,9 +65554,7 @@ - - - +

da erfahrungsgemäß der OS-Scheduler immer eine gewisse Latenz hat, bis ein Thread tatsächlich ausführbar wird @@ -65573,9 +65572,7 @@ - - - +

und trotzdem: das ist kein Gegenbeweis @@ -65589,9 +65586,7 @@ - - - +

man könnte das Thema entschärfen, @@ -65685,7 +65680,7 @@ - + @@ -65784,8 +65779,10 @@ - - + + + + @@ -65836,15 +65833,13 @@ - - + + - - - +

es muß ein Konstruktor-Aufruf sein @@ -65852,9 +65847,7 @@ - - - +

zwar könnte man nun generell an eine Factory denken, aber diese verlagert die gesamten Gefahren mit dem Instance-Management auf den usage-Kontext, und untergräbt damit den Zweck des Unterfangens... @@ -65864,9 +65857,7 @@ - - - +

er soll einfach und sprechend sein @@ -65877,9 +65868,7 @@ - - - +

der Einstieg muß zuverlässig im Typ verankert sein @@ -65890,9 +65879,7 @@ - - - +

also.... @@ -65926,14 +65913,12 @@ - - + + - - - +

er bietet als internes API eine Funktion configure(this) @@ -65950,9 +65935,7 @@ - - - +

da bin ich schon x-mal darüber gestolpert: man kann per Reference oder per Value initialisieren; was nicht funktioniert ist eine move-Initialisierung, d.h. jeder move-only Argument-Typ macht Probleme. Einziger Ausweg war für mich bisher, die captures explizit zu benennen und per std::forward-assign zu initialisieren @@ -65970,9 +65953,7 @@ - - - +

wenn halt Argument-Packs einfach als Typ repräsentierbar wären — aber dem ist nicht so; man muß gegen ein getemplatetes Argument matchen, um aus einem Argument-Pack einen anderen Argument-Pack zu konstruieren @@ -66003,11 +65984,14 @@ - - + + - - + + + + + @@ -66036,13 +66020,16 @@ - + + - + + + @@ -66054,6 +66041,11 @@ + + + + + @@ -66137,12 +66129,20 @@ - + + + + + + + + + @@ -95642,9 +95642,7 @@ class Something - - - +

Einmal-Event beim Ausführen der Testsuite nach Compile.