diff --git a/src/lib/thread.hpp b/src/lib/thread.hpp index 970bd66e7..69deb4b94 100644 --- a/src/lib/thread.hpp +++ b/src/lib/thread.hpp @@ -52,6 +52,11 @@ ** As a convenience, the destructor blocks for a short timespan of 20ms; a thread running ** beyond that grace period will kill the whole application by `std::terminate`. ** + ** For the exceptional case when a supervising thread need to await the termination of + ** launched threads, a different front-end \ref lib::ThreadJoinable is provided, exposing + ** the `join()` operation. Such threads *must* be joined however, and thus the destructor + ** immediately terminates the application in case the thread is still running. + ** ** ## 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_ @@ -106,37 +111,36 @@ #include #include #include +#include namespace lib { using std::string; + namespace thread {// Thread-wrapper base implementation... + template class ThreadWrapper : util::MoveOnly { - template void main (string threadID, FUN&& threadFunction, ARGS&& ...args) { + markThreadStart (threadID); try { - markThreadStart (threadID); - // execute the actual operation in this new thread + // execute the actual operation in this new thread std::invoke (std::forward (threadFunction), std::forward (args)...); } ERROR_LOG_AND_IGNORE (thread, "Thread function") + // + markThreadEnd (threadID); + if (autoTerm) + threadImpl_.detach(); } - void - markThreadStart (string const& threadID) - { - string logMsg = util::_Fmt{"Thread '%s' start..."} % threadID; - TRACE (thread, "%s", logMsg.c_str()); - //////////////////////////////////////////////////////////////////////OOO maybe set the the Thread-ID via POSIX ?? - } protected: std::thread threadImpl_; @@ -144,6 +148,11 @@ namespace lib { /** @internal derived classes may create an inactive thread */ ThreadWrapper() : threadImpl_{} { } + ~ThreadWrapper() + { + if (autoTerm and threadImpl_.joinable()) + waitGracePeriod(); + } public: /** Create a new thread to execute the given operation. @@ -187,6 +196,42 @@ namespace lib { { return threadImpl_.get_id() == std::this_thread::get_id(); } // Note: implies get_id() != std::thread::id{} ==> it is running + + private: + void + markThreadStart (string const& threadID) + { + string logMsg = util::_Fmt{"Thread '%s' start..."} % threadID; + TRACE (thread, "%s", logMsg.c_str()); + //////////////////////////////////////////////////////////////////////OOO maybe set the the Thread-ID via POSIX ?? + } + + void + markThreadEnd (string const& threadID) + { + string logMsg = util::_Fmt{"Thread '%s' finished..."} % threadID; + TRACE (thread, "%s", logMsg.c_str()); + } + + void + waitGracePeriod() noexcept + { + using std::chrono::steady_clock; + using std::chrono_literals::operator ""ms; + + try { + auto start = steady_clock::now(); + while (threadImpl_.joinable() + and steady_clock::now () - start < 20ms + ) + std::this_thread::yield(); + } + ERROR_LOG_AND_IGNORE (thread, "Thread shutdown wait") + + if (threadImpl_.joinable()) + ALERT (thread, "Thread failed to terminate after grace period. Abort."); + // invocation of std::thread dtor will presumably call std::terminate... + } }; }//(End)base implementation. @@ -204,7 +249,7 @@ namespace lib { * `std::terminate` afterwards, should the thread still be active then. */ class Thread - : public thread::ThreadWrapper + : public thread::ThreadWrapper { public: @@ -216,35 +261,29 @@ namespace lib { - /** - * Variant of the standard case, allowing additionally - * to join on the termination of this thread. + /************************************************************************//** + * Variant of the [standard case](\ref Thread), requiring to wait and `join()` + * on the termination of this thread. Useful to collect results calculated + * by multiple threads. Note however that the system resources of the thread + * are kept around until the `join()` call, and thus also the `bool` conversion + * yields `true`, even while the actual operation has already terminated. + * @warning Thread must be joined prior to destructor invocation, otherwise + * the application is shut down immediately via `std::terminate`. */ class ThreadJoinable - : public thread::ThreadWrapper + : public thread::ThreadWrapper { public: using ThreadWrapper::ThreadWrapper; - /** put the caller into a blocking wait until this thread has terminated. - * @return token signalling either success or failure. - * The caller can find out by invoking `isValid()` - * or `maybeThrow()` on this result token - */ - lib::Result + /** put the caller into a blocking wait until this thread has terminated */ + void join () { -// if (!isValid()) -// throw error::Logic ("joining on an already terminated thread"); -// -// lumiera_err errorInOtherThread = -// "TODO TOD-oh";//lumiera_thread_join (threadHandle_); //////////////////////////////////OOO -// threadHandle_ = 0; -// -// if (errorInOtherThread) -// return error::State ("Thread terminated with error", errorInOtherThread); -// else -// return true; + if (not threadImpl_.joinable()) + throw error::Logic ("joining on an already terminated thread"); + + threadImpl_.join(); } }; diff --git a/tests/basics/diagnostic-context-test.cpp b/tests/basics/diagnostic-context-test.cpp index 75756c4a1..f08949471 100644 --- a/tests/basics/diagnostic-context-test.cpp +++ b/tests/basics/diagnostic-context-test.cpp @@ -165,7 +165,8 @@ namespace test{ TestThread testcase[NUM_THREADS] SIDEEFFECT; for (uint i=0; i < NUM_THREADS; ++i) - CHECK (testcase[i].join().isValid() ); + testcase[i].join(); +// CHECK (testcase[i].join().isValid() ); ////////////////////////////////////////////OOO need a way to pass the verification-Result. Maybe a Future? } diff --git a/tests/core/steam/control/session-command-function-test.cpp b/tests/core/steam/control/session-command-function-test.cpp index f09493684..f2633eda2 100644 --- a/tests/core/steam/control/session-command-function-test.cpp +++ b/tests/core/steam/control/session-command-function-test.cpp @@ -345,7 +345,7 @@ namespace test { ~InvocationProducer() { - this->join().maybeThrow(); + this->join(); // .maybeThrow(); /////////////////////////////////////////OOO should detect exceptions in thread explicitly for (auto& id : cmdIDs_) Command::remove (cStr(id)); } diff --git a/tests/library/thread-wrapper-join-test.cpp b/tests/library/thread-wrapper-join-test.cpp index b451ed813..1ba9acd68 100644 --- a/tests/library/thread-wrapper-join-test.cpp +++ b/tests/library/thread-wrapper-join-test.cpp @@ -65,7 +65,6 @@ namespace test { { simpleUse (); wrongUse (); - getError (); } @@ -111,25 +110,6 @@ namespace test { VERIFY_ERROR(LOGIC, newThread.join() ); VERIFY_ERROR(LOGIC, newThread.join() ); } - - - void - getError() - { - ThreadJoinable thread1("test Thread joining-3" - , bind (&ThreadWrapperJoin_test::theAction, this, DESTRUCTION_CODE) - ); - - VERIFY_ERROR(SPECIAL, thread1.join().maybeThrow() ); - - - - ThreadJoinable thread2("test Thread joining-4" - , bind (&ThreadWrapperJoin_test::theAction, this, DESTRUCTION_CODE) - ); - - CHECK (not thread2.join().isValid()); // can check success without throwing - } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index f5011509b..a990804fa 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -79390,6 +79390,138 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + +

+ ...da die Implementierung mit einem aktiven Threadpool verbunden war, gab es dort auch eine Managment-Datenstruktur; die Threadpool-Logik hat eine eventuell im Thread noch erkannte Fehlerflag dorthin gespeichert — und join() konnte diese Fehlerflag dann einfach abholen. +

+ + +
+
+ + + + + + +

+ ...C kennt ja keine Exceptions — und der Author des Threadpool-Frameworks hielt Exceptions für ein bestenfalls überflüssiges Feature, das es ganz bestimmt nicht rechtfertigt, zusätzlichen Aufwand zu treiben +

+ + +
+
+ + + + + + +

+ gemeint ist lib::Result +

+ + +
+
+ + + + + + + + + + +

+ ...und zwar im Besonderen, wenn man in einer »reaping-loop« alle Kind-Threads ernten möchte; dann würde nur ein Fehler ausgeworfen, und die weiteren Kinder bleiben ungeerntet (und terminieren jetzt, mit der C++14-Lösung sogar das Programm) +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Das ist ein klassischer Fall für ein Feature, das zu Beginn so offensichtlich und irre nützlich aussieht — dann aber in der Realität nicht wirklich „fliegt“ +

+ + +
+
+ + + + + + +

+ heute bevorzugt man für ähnliche Anforderungen das Future / Promise - Konstrukt +

+ + +
+
+ + + + + + + +

+ Denn tatsächlich sind aufgetretene Fehler dann ehr schon eine Form von Zustand, den man mit einem speziellen Protokoll im Thread-Objekt erfassen und nach dem join() abfragen sollte; so kann man auch die Ergebnisse mehrerer Threads korrekt kombinieren +

+ + +
+ +
+ + + +
+
@@ -79819,6 +79951,28 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + + + + + + + + + + + + + + +