diff --git a/src/lib/thread.hpp b/src/lib/thread.hpp index eb3560f10..fa32efe83 100644 --- a/src/lib/thread.hpp +++ b/src/lib/thread.hpp @@ -352,6 +352,8 @@ namespace lib { * 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 only uses storage within its own scope. + * @deprecated can't sleep well while this function is exposed; + * need a prime solution to address this relevant use case ////////////////////////////////////////OOO allow for a thread with explicit lifecycle */ void detach() { ThreadLifecycle::handle_end_of_thread(); } }; diff --git a/src/stage/gtk-lumiera.cpp b/src/stage/gtk-lumiera.cpp index e9f1de535..fd64a3b02 100644 --- a/src/stage/gtk-lumiera.cpp +++ b/src/stage/gtk-lumiera.cpp @@ -149,7 +149,7 @@ namespace stage { { try { - Thread {"GUI-Main", bind (&runGUI, terminationHandle)}; + Thread {"GUI-Main", bind (&runGUI, terminationHandle)}; ///////////////////////////////////////////OOO this shows we need a self-contained and detached thread! return true; // if we reach this line... } catch(...) diff --git a/src/steam/control/steam-dispatcher.cpp b/src/steam/control/steam-dispatcher.cpp index 702c71204..5b7b321eb 100644 --- a/src/steam/control/steam-dispatcher.cpp +++ b/src/steam/control/steam-dispatcher.cpp @@ -263,7 +263,7 @@ namespace control { } // leave the Session thread... // send notification of subsystem shutdown - thread_.detach(); + thread_.detach();/////////////////////////////////////////////////////////////OOO while this case is exceptional, it still mandates better framework support notifyEnd (&errorMsg); // invokes ~DispatcherLoop() } diff --git a/tests/11concurrency.tests b/tests/11concurrency.tests index 40ee2df2e..248c18f84 100644 --- a/tests/11concurrency.tests +++ b/tests/11concurrency.tests @@ -42,6 +42,16 @@ return: 0 END +PLANNED "Launch self-contained detached Thread" ThreadWrapperAutonomous_test < + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +* *****************************************************/ + +/** @file thread-wrapper-autonomous-test.cpp + ** unit test \ref ThreadWrapperAutonomous_test + */ + + +#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 +#include + +using test::Test; +using lib::explore; +using std::atomic_uint; +using std::this_thread::yield; +using std::this_thread::sleep_for; +using std::chrono::microseconds; + + +namespace lib { +namespace test{ + + namespace { // test parameters + + const uint NUM_THREADS = 200; + const uint REPETITIONS = 10; + } + + + /*******************************************************************//** + * @test a variation of the Thread wrapper to launch a detached thread, + * with automatic memory management for the _thread-object._. + * @see thread.hpp + * @see ThreadWrapper_test + */ + class ThreadWrapperAutonomous_test : public Test + { + + virtual void + run (Arg) + { + demonstrateSimpleUsage(); + verifyMemoryManagement(); + } + + + /** @test demonstrate simply launching a λ-function into background */ + void + demonstrateSimpleUsage() + { + atomic_uint i{0}; + Thread thread("counter", [&]{ ++i; }); // bind a λ and launch thread + while (thread) yield(); // ensure thread has finished and detached + + CHECK (i == 1); // verify the effect has taken place + UNIMPLEMENTED ("actually launch detached"); + } + + + /** + * @test verify the detached thread autonomously manages its memory. + */ + void + verifyMemoryManagement() + { + struct TestThread + : Thread + { + using Thread::Thread; + + uint local{0}; + + void + doIt (uint a, uint b) ///< the actual operation running in a separate thread + { + uint sum = a + b; + sleep_for (microseconds{sum}); // Note: explicit random delay before local store + local = sum; + } + }; + + // prepare Storage for these objects (not created yet) + lib::ScopedCollection threads{NUM_THREADS}; + + size_t checkSum = 0; + size_t globalSum = 0; + auto launchThreads = [&] + { + for (uint i=1; i<=NUM_THREADS; ++i) + { + uint x = rand() % 1000; + globalSum += (i + x); + threads.emplace (&TestThread::doIt, i, x); + } // Note: bind to member function, copying arguments + + while (explore(threads).has_any()) + yield(); // wait for all threads to have detached + + for (auto& t : threads) + { + CHECK (0 < t.local); + checkSum += t.local; + } + }; + + double runTime = benchmarkTime (launchThreads, REPETITIONS); + + CHECK (checkSum == globalSum); // sum of precomputed random numbers matches sum from threads + CHECK (runTime < NUM_THREADS * 1000/2); // random sleep time should be > 500ms on average + } + }; + + + + /** Register this test class... */ + LAUNCHER (ThreadWrapperAutonomous_test, "function common"); + + + +}} // namespace lib::test diff --git a/tests/library/thread-wrapper-lifecycle-test.cpp b/tests/library/thread-wrapper-lifecycle-test.cpp new file mode 100644 index 000000000..9022747f2 --- /dev/null +++ b/tests/library/thread-wrapper-lifecycle-test.cpp @@ -0,0 +1,146 @@ +/* + ThreadWrapperLifecycle(Test) - verify lifecycle aspects of the thread wrapper + + Copyright (C) Lumiera.org + 2023, Hermann Vosseler + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +* *****************************************************/ + +/** @file thread-wrapper-lifecycle-test.cpp + ** unit test \ref ThreadWrapperLifecycle_test + */ + + +#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 +#include + +using test::Test; +using lib::explore; +using std::atomic_uint; +using std::this_thread::yield; +using std::this_thread::sleep_for; +using std::chrono::microseconds; + + +namespace lib { +namespace test{ + + namespace { // test parameters + + const uint NUM_THREADS = 200; + const uint REPETITIONS = 10; + } + + + /*******************************************************************//** + * @test verify lifecycle behaviour of threads managed by thread-wrapper. + * @see thread.hpp + * @see ThreadWrapperBackground_test + * @see ThreadWrapperJoin_test + */ + class ThreadWrapperLifecycle_test : public Test + { + + virtual void + run (Arg) + { + defaultWrapperLifecycle(); + verifyExplicitLifecycleState(); + } + + + /** @test demonstrate terms of lifecycle for the default case */ + void + defaultWrapperLifecycle() + { + atomic_uint i{0}; + Thread thread("counter", [&]{ ++i; }); // bind a λ and launch thread + while (thread) yield(); // ensure thread has finished and detached + + CHECK (i == 1); // verify the effect has taken place + UNIMPLEMENTED ("demonstrate state change"); + } + + + /** + * @test verify a special setup to start a thread explicitly and to track + * the thread's lifecycle state. + */ + void + verifyExplicitLifecycleState() + { + struct TestThread + : Thread + { + using Thread::Thread; + + uint local{0}; + + void + doIt (uint a, uint b) ///< the actual operation running in a separate thread + { + uint sum = a + b; + sleep_for (microseconds{sum}); // Note: explicit random delay before local store + local = sum; + } + }; + + // prepare Storage for these objects (not created yet) + lib::ScopedCollection threads{NUM_THREADS}; + + size_t checkSum = 0; + size_t globalSum = 0; + auto launchThreads = [&] + { + for (uint i=1; i<=NUM_THREADS; ++i) + { + uint x = rand() % 1000; + globalSum += (i + x); + threads.emplace (&TestThread::doIt, i, x); + } // Note: bind to member function, copying arguments + + while (explore(threads).has_any()) + yield(); // wait for all threads to have detached + + for (auto& t : threads) + { + CHECK (0 < t.local); + checkSum += t.local; + } + }; + + double runTime = benchmarkTime (launchThreads, REPETITIONS); + + CHECK (checkSum == globalSum); // sum of precomputed random numbers matches sum from threads + CHECK (runTime < NUM_THREADS * 1000/2); // random sleep time should be > 500ms on average + } + }; + + + + /** Register this test class... */ + LAUNCHER (ThreadWrapperLifecycle_test, "function common"); + + + +}} // namespace lib::test diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index ed8c1116b..f50119019 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -64286,6 +64286,1182 @@ + + + + + + + + + + + + + + + + + + + +

YAGNI.

+

+ Zunächst einmal so wenig Funktionalität wie möglich durchreichen, und das nur auf den festen Bahnen gemäß Design; im Zweifelsfall ist es besser, spezielle Funktionalität im Bedarfsfall in den Wrapper zu packen; genau das ist ja der Vorteil einer Hilfsklasse direkt im Projekt. +

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

+ Zwar sichert der Standard zu, daß das Ende des ctor-Aufrufs synchronizes_with  dem Start der Thread-Funktion. Streng logisch kann das aber nur für den std::thread-Konstruktor selber gelten (andernfalls hätte man mit einem Sequence-Point argumentieren müssen, und nicht mit dem ctor selber; das würde dann aber auch wieder eine unerwünschte Statefulness einführen, weil dann im gesamten umschließenden Ausdruck der Thread eben noch nicht läuft, was das RAII-Konzept untergraben würde). +

+
+

+ Das ist nun zwar ziemlich unwahrscheinlich (weil normalerweise der Scheduler immer eine erhebliche Zeit braucht, bis ein anderer Thread überhaupt zum Zug kommt), aber leider ist es demzufolge theoretisch möglich, daß eine im abgeleiteten Objekt definierte Thread-Funktionalität bereits auf eine noch nicht vollständig initialisierte Objektinstanz zugreift, oder daß die Initialisierung der abgeleiteten Klasse Werte in lokalen Feldern überschreibt, die aus der bereits startenden Thread-Funktion gesetzt wurden. Unerklärliches und reproduzierbares Verhalten wäre die Folge. Und das läßt sich aus dem Wrapper heraus nicht beheben (zumindest nicht ohne verwirrende Konventionen einzuführen). +

+ +
+ +
+ + + + + + +

+ explizit eine lib::SyncBarrier in der Implementierung einbinden +

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

+ ...damit man auch zusammengesetzte/formatierte Werte bauen kann +

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

+ std::thread::native_handle() ⟼ liefert hier pthread_t +

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

+ es wurde von Race-Problemen berichtet, und davon, daß der zuletzt gesetzte Identifier plötzlich auf allen Threads auftaucht +

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

+ sollte dann den this-Typ extrahieren und den this-Ptr automatisch injizieren +

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

+ es ergeben sich zwei Schwierigkeiten... +

+
    +
  • + eine Varargs-Variante würde die bestehenden Overloads kanibalisieren bzw. würde für 1-2 Argumente einen "ambiguous overload" provozieren +
  • +
  • + mit einer Varargs-Variante gibt es keine Möglichkeit, den Delimiter anzugeben +
  • +
+ +
+ +
+ + + + + + + + + + +

+ Er ist aus mehreren Gründen gut +

+
    +
  • + er ist bereits konventionell; im Besonderen in der C-Marko-Form (die Lumiera auch verwendet) +
  • +
  • + er teilt sich sprachlich gut mit +
  • +
+

+ Aber es ist ein sehr fester Name; er ermöglicht kaum Verkürzungen oder Varianten +

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

+ das macht nur den Code komplex, macht aber die Diagnostik nicht besser; std::invoke hat bereits gute Diagnostik (man muß sie nur lesen können) +

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

+ der Name ist nicht besonders klar +

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

+ Die hatte ich eingebaut, um für spezialisierte abgeleitete Klassen doch noch erweiterte Zustandsübergänge zu ermöglichen +

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

+ ...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 +

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

+ Knoten durchhauen! +

+

+ Warum quäle ich mich damit herum, was ich künftig dürfen darf, und was unangemessen wäre? Meine Einstellung ist doch, daß eine Library strukturieren sollte, aber nicht vorgreifen und bestimmen. Wenn die letzten 10 Jahre eines gezeigt haben, dann den Umstand, daß der Thread-Wrapper vor allem für Tests verwendet wird. Und genau da gelten die ganzen Performance- und Architektur-Überlegungen überhaupt nicht; sondern es kommt auf die den Umständen entsprechende, gradlinige Formulierung im Testcode an. Und da könnte unter Umständen Fork-Join genau die KISS-Lösung sein. Insofern wäre das Design bereits nahezu gelungen: es trennt einen intendierten Standardfall ab, läßt aber sonst alle Türen offen.... +

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

+ und kann zugleich jedwede Exception halten +

+ +
+
+
+ + + + + +

+ und ansonsten aber left-biassed sein +

+ +
+
+ + + + +

+ incl perfect forwarding +

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

+ Es wäre zwar so irgendwie hinzubekommen — aber weit entfernt von einem guten Design. +

+

+ Ich baue hier eine auf Dauer angelegte Lösung, die auch sichtbar ist und in Frage gestellt werden wird! +

+ +
+ + + + + + +

+ hab das gestern versucht — so kann das nicht stehen bleiben, das ist ja lächerlich (brauche explizite Template-Instantiierung) — zumindest solange wir keine C++-Module verwenden (C++20) +

+ +
+
+ + + + +

+ die Code-Anordnung hat keinen Flow +

+ +
+ +
+ + + + +

+ Ich habe doch selber oben diese Prinzipien formuliert: eine Library-Lösung sollte nicht mutwillig beschränken, sondern eine sinnvolle Gliederung anbieten... +

+ +
+ + +
+
+ + + + +

+ also vielleicht doch Policy-based-Design? +

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

+ ⟹ wenn überhaupt, müßte die Policy einen mittleren Binding-Layer bilden +

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

+ Uh-Oh ... jetzt bin ich richtig stolz auf mich... +

+ +
+ + +
+ + + + +

+ ...oder gut gegliedert; das jetzt gefundene Design trennt nämlich das Starten der Funktion, Fangen von Fehlern und Speichern von Ergebnissen als eigenen Belang ab (⟶ lib::Result); dadurch wird die Policy nun wirklich kurz und klar +

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

+  Thread  main<FUN,ARGS...> +

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

+ Hier zeigt sich ein Widerspruch in den Konzepten selber an +

+
    +
  • + ein »launch-only«-Thread sollte sich eigentlich komplett abkoppeln +
  • +
  • + aber andererseits soll das Thead-Objekt auch den zugehörigen State kapseln, muß also irgendwo existieren +
  • +
+ +
+
+ + + + + + + +

+ dieses habe ich als RAII-Objekt angelegt, und der zugehörige Unique-Ptr markiert gleichzeitig den Lifecycle-State; das hat zur Konsequenz, daß der Session-Thread am Ende selber sein eigenes Objekt zerstören muß +

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

+ ...diese erzwingen, daß das Thread-Objekt während der gesamten Ausführung der Thread-Funktion erhalten bleibt; dies Design erscheint auch sinnvoll +

+ +
+
+ + + + +

+ der bisherige Lumiera-Threadwrapper hat zwar ein Thread-Handle gehalten, das aber letztlich beim Threadpool registriert war. Daher war das Thread-Objekt selber im Grunde verzichtbar (es sei denn, man hat davon abgeleitet und dort weitere Felder untergebracht) +

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

+ der Thread selber könnte (müßte aber nicht) +

+

+ am Ende den optional auf »empty« setzen +

+ +
+ + + +

+ ...das müßte dann so aufgebaut werden, wie es derzeit beim Session-Thread geschieht (nur daß dort aktuell ein unique_ptr und kein optional diese Möglichkeit schafft); das heißt, die Thread-Funktion muß irgendwie eine Rückreferenz auf den umschließenden Scope haben, sie muß auf ihrem eigenen Handle schließlich ein detach() aufrufen, bevor sie den Manager auf »leer« setzt, und sie sollte unmittelbar danach enden. +

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

+ reguläre Threads sind auch nicht beeinflußbar, es sei denn, man programmiert das explizit... Und verklemmen können se sich immer +

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

+ ...und wenn ein Thread nicht joinable ist, bringt es oft nichts +

+ +
+ +
+
+ + + + +

+ Antwort: ja — es kann dafür gute Gründe geben +

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

+ letzten Endes ist das hier keine general purpose Library — es wird ein interner Nutzer voraussgesetzt, die die betroffenen Belange im Prinzip versteht (und durch die Library lediglich rascher zu klarererm Code kommt) +

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

+ man müßte das entweder direkt in den Konstruktor einbauen — und das wäre eine Verschlechterung, denn bisher sind die Konstruktoren mehr oder weniger orthogonal zu den Policies +

+ +
+
+ + + + +

+ es würde zwar genau diesen einen Fall lösen, aber keinen Zugang zur Lösung ähnlich gelagerter Probleme bieten — welche im Besonderen (siehe Session-Thread) dann mit dem threadID-String zu kämpfen haben, und spezielle Einschränkungen die Storage betreffend zu beachten haben; es wäre besser, wenn man das herausgeführte detach() wieder loswerden könnte! +

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

+ und er ist semantisch genau für diesen Zweck gedacht +

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

+ �� ...der Thread-Funktor (innen) managed seine eigene + Hülle (Thread-Objekt) — ziemlich irre diese Lösung +

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

+ das ist ziemlich einfach und natürlich bei der gebenen Code-Struktur — die „schreit“ ja gradezu danach, daß man seine Finger von thread::ThreadLifecycle lassen soll +

+ +
+
+ + + + +

+ das wäre der schwierige Part, denn normalerweise kann immer jemand anderen Code daneben stellen, der diese Bausteine geschickt anders verwendet — und aller Erfahrung nach könnte dieser „jemand“ ich selber sein... +

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

+ da nicht der Thread-Wrapper selber damit überladen wird +

+ +
+
+ + + + + +

+ user bekommt einen Typ OptionalThread ≡ ein opaquer maßgeschneiderter Wrapper +

+ +
+
+ + + + + +

+ das wäre nicht zwingend so; viel einfacher wäre die Implementierung, wenn man bloß Variante-2 weiter ausbaut; allerdings impliziert ein Thread-Objekt eben genau die Verwendung von lokaler Storage, und ein mit dem Thread-Ende gekoppelter Destruktor-Aufruf ist nützlich und einfach zu realisieren. +

+ +
+
+
+ + + + +

+ Das bedeutet, man bekommt in diesem Fall sogar eine Barriere geschenkt, die man dann nicht mehr mit SyncBarrier explizit coden muß. Einzige Vorraussetzung: prüfen und verzweigen auf den Lebenszyklus-Zustand +

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

+ Im Klartext: der Thread-Wrapper muß immer während der Ausführung der Thread-Funktion am Leben bleiben; die im Spezialfall notwendigen Ausnahmen sind in der Policy selber verborgen. +

+ +
+
+ + + + +

+ das heißt: keine Tricks mehr, indem man im laufenden Thread das Desaster nur umschifft. Der threadID-string liegt einfach im ThreadWrapper und basta. Und es gibt keine „besseren“ Statusvariablen mehr, die im Thread-Funktor leben müssen +

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

+ dies hier ist eine Application-Support-Library; die Nutzer kommen intern aus dem Projekt — Verständnis der Belange wird vorausgesetzt; wer die Library ihren Möglichkeiten gemäß nutzt, ist sicher, wer Bausteine aus der Library mit eigenem Code verbindet, sollte wissen, was er tut +

+ +
+ +
+ + + + +

+ Hintertüren wie das detach() gibt es künftig nicht mehr — und die Destruktoren terminieren die Applikation, wenn der Thread noch lebt +

+ +
+
+ + + + + + + +

+ es gibt die theoretische Möglichkeit für ein Memory-Leak +

+ +
+ + + + + +

+ ...und zwar müßte +

+
    +
  • + die Einrichtung des Thread ohne Probleme möglich sein (sonst würde der ctor von std::thread scheitern) +
  • +
  • + das Kopieren des Funktors und der Argumente ebenfalls ohne Probleme erfolgt sein +
  • +
  • + aber danach ein Problem im bereits eingerichteten Thread auftreten, bevor unser Code den Manager (smart-ptr) aktivieren kann. +
  • +
+

+ Hierzu kommt nur wenig in Frage: einmal die Invocation des Funktors selber mit als Wert vorliegenden Parametern, sowie dann unser eigener Library-Code bis zu der Stelle, an der der Pointer gesichert ist. Da sehe ich im Moment wenig Raum für Fehler. Funktoren sind ja entweder Pointer oder Objekte, und durch die erzwungene Kopie sind die tatsächlich dann aktiven Instanzen bereits konstruiert. Ein std::function verwendet zwar einen Invoker, aber das ist ein Trampolin, um die verschiedenen Aufruf-Technologien zu nivellieren; mir ist nicht bekannt, daß das für den Aufruf noch irgend etwas macht, was scheitern könnte. Bleiben also nur noch spezielle esoterische Argument-Typen. Und außerdem müßte der Optimiser so dämlich sein, ein bereits kopiertes Argument noch einmal zu kopieren nur für den Aufruf. Abgesehen davon könnten diese »esoterischen« Typen nur bei den weiteren Funktions-Argumenten auftreten (nicht der Funktor, nicht der this-Pointer). Danach haben wir nur noch einen nicht-virtuellen Aufruf in die Policy (ist inline) und das speichern des this-Pointers in den smart-ptr. Wenn danach noch was passiert, terminiert der Thread, der Funktor wird de-alloziert, und der darin eingebettete smart-ptr waltet seines Amtes +

+ +
+
+ + + + +

+ Diese Library ist für ein Konzept von »Thread« entworfen, das sicherlich nicht ermöglicht, solche Massen an Threads zu erzeugen, daß ein Leak sich praktisch bemerkbar machen würde.... +

+ +
+
+
+ + + + + +

+ ...man kann es so einrichten, daß zuerst das detach() und dann der Destruktor-Aufruf wirklich als Letztes im Thread passieren, und danach nur noch der Funktor vom Framework de-alloziert wird. Falls hier was schief geht, terminiert die Applikation (und das ist gut so) +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -79295,773 +80471,10 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + - - - - - - - - - - -

- YAGNI. -

-

- Zunächst einmal so wenig Funktionalität wie möglich durchreichen, und das nur auf den festen Bahnen gemäß Design; im Zweifelsfall ist es besser, spezielle Funktionalität im Bedarfsfall in den Wrapper zu packen; genau das ist ja der Vorteil einer Hilfsklasse direkt im Projekt. -

- -
- -
-
- - - - - - - - - - -

- Zwar sichert der Standard zu, daß das Ende des ctor-Aufrufs synchronizes_with  dem Start der Thread-Funktion. Streng logisch kann das aber nur für den std::thread-Konstruktor selber gelten (andernfalls hätte man mit einem Sequence-Point argumentieren müssen, und nicht mit dem ctor selber; das würde dann aber auch wieder eine unerwünschte Statefulness einführen, weil dann im gesamten umschließenden Ausdruck der Thread eben noch nicht läuft, was das RAII-Konzept untergraben würde). -

-
-

- Das ist nun zwar ziemlich unwahrscheinlich (weil normalerweise der Scheduler immer eine erhebliche Zeit braucht, bis ein anderer Thread überhaupt zum Zug kommt), aber leider ist es demzufolge theoretisch möglich, daß eine im abgeleiteten Objekt definierte Thread-Funktionalität bereits auf eine noch nicht vollständig initialisierte Objektinstanz zugreift, oder daß die Initialisierung der abgeleiteten Klasse Werte in lokalen Feldern überschreibt, die aus der bereits startenden Thread-Funktion gesetzt wurden. Unerklärliches und reproduzierbares Verhalten wäre die Folge. Und das läßt sich aus dem Wrapper heraus nicht beheben (zumindest nicht ohne verwirrende Konventionen einzuführen). -

- -
- -
- - - - - - -

- explizit eine lib::SyncBarrier in der Implementierung einbinden -

- -
-
- - -
-
- - - - - - - - - - - - - - - - - -

- ...damit man auch zusammengesetzte/formatierte Werte bauen kann -

- -
- -
- - - - - - - - - - - - - - - - - -

- std::thread::native_handle() ⟼ liefert hier pthread_t -

- -
-
- - - - - - - - - - - - - - -

- es wurde von Race-Problemen berichtet, und davon, daß der zuletzt gesetzte Identifier plötzlich auf allen Threads auftaucht -

- -
- -
-
-
-
- - - - - - - - - - - - - -

- sollte dann den this-Typ extrahieren und den this-Ptr automatisch injizieren -

- -
- - - - - - - - - - - - - - - - - - -

- es ergeben sich zwei Schwierigkeiten... -

-
    -
  • - eine Varargs-Variante würde die bestehenden Overloads kanibalisieren bzw. würde für 1-2 Argumente einen "ambiguous overload" provozieren -
  • -
  • - mit einer Varargs-Variante gibt es keine Möglichkeit, den Delimiter anzugeben -
  • -
- -
- -
- - - - - - - - - - -

- Er ist aus mehreren Gründen gut -

-
    -
  • - er ist bereits konventionell; im Besonderen in der C-Marko-Form (die Lumiera auch verwendet) -
  • -
  • - er teilt sich sprachlich gut mit -
  • -
-

- Aber es ist ein sehr fester Name; er ermöglicht kaum Verkürzungen oder Varianten -

- -
-
- - - - - - - - - - - - - - - - -

- das macht nur den Code komplex, macht aber die Diagnostik nicht besser; std::invoke hat bereits gute Diagnostik (man muß sie nur lesen können) -

- -
- -
-
-
-
- - - - - - - - - - - - - - -

- der Name ist nicht besonders klar -

- -
- -
- - - - - - - -

- Die hatte ich eingebaut, um für spezialisierte abgeleitete Klassen doch noch erweiterte Zustandsübergänge zu ermöglichen -

- -
-
-
-
- - - - -
- - - - - - -

- ...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 -

- -
- -
- - - - - - - - - - - - - - - - - -

- Knoten durchhauen! -

-

- Warum quäle ich mich damit herum, was ich künftig dürfen darf, und was unangemessen wäre? Meine Einstellung ist doch, daß eine Library strukturieren sollte, aber nicht vorgreifen und bestimmen. Wenn die letzten 10 Jahre eines gezeigt haben, dann den Umstand, daß der Thread-Wrapper vor allem für Tests verwendet wird. Und genau da gelten die ganzen Performance- und Architektur-Überlegungen überhaupt nicht; sondern es kommt auf die den Umständen entsprechende, gradlinige Formulierung im Testcode an. Und da könnte unter Umständen Fork-Join genau die KISS-Lösung sein. Insofern wäre das Design bereits nahezu gelungen: es trennt einen intendierten Standardfall ab, läßt aber sonst alle Türen offen.... -

- -
- - - - -
- - - - -
-
- - - - - - - - - - - - - - - - - - - - -

- und kann zugleich jedwede Exception halten -

- -
-
-
- - - - - -

- und ansonsten aber left-biassed sein -

- -
-
- - - - -

- incl perfect forwarding -

- -
- - -
- - - -
- - - - -
-
- - - - - - - - - - - - - - - -

- Es wäre zwar so irgendwie hinzubekommen — aber weit entfernt von einem guten Design. -

-

- Ich baue hier eine auf Dauer angelegte Lösung, die auch sichtbar ist und in Frage gestellt werden wird! -

- -
- - - - - - -

- hab das gestern versucht — so kann das nicht stehen bleiben, das ist ja lächerlich (brauche explizite Template-Instantiierung) — zumindest solange wir keine C++-Module verwenden (C++20) -

- -
-
- - - - -

- die Code-Anordnung hat keinen Flow -

- -
- -
- - - - -

- Ich habe doch selber oben diese Prinzipien formuliert: eine Library-Lösung sollte nicht mutwillig beschränken, sondern eine sinnvolle Gliederung anbieten... -

- -
- - -
-
- - - - -

- also vielleicht doch Policy-based-Design? -

- -
- - - - - - - - - - - - -

- ⟹ wenn überhaupt, müßte die Policy einen mittleren Binding-Layer bilden -

- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- Uh-Oh ... jetzt bin ich richtig stolz auf mich... -

- -
- - -
- - - - -

- ...oder gut gegliedert; das jetzt gefundene Design trennt nämlich das Starten der Funktion, Fangen von Fehlern und Speichern von Ergebnissen als eigenen Belang ab (⟶ lib::Result); dadurch wird die Policy nun wirklich kurz und klar -

- -
-
-
- - - - - - - -

-  Thread  main<FUN,ARGS...> -

- -
-
-
- - -
-
- - - - - - - - -

- Hier zeigt sich ein Widerspruch in den Konzepten selber an -

-
    -
  • - ein »launch-only«-Thread sollte sich eigentlich komplett abkoppeln -
  • -
  • - aber andererseits soll das Thead-Objekt auch den zugehörigen State kapseln, muß also irgendwo existieren -
  • -
- -
-
- - - - - - - - - -

- dieses habe ich als RAII-Objekt angelegt, und der zugehörige Unique-Ptr markiert gleichzeitig den Lifecycle-State; das hat zur Konsequenz, daß der Session-Thread am Ende selber sein eigenes Objekt zerstören muß -

- -
- - -
- - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -

- ...diese erzwingen, daß das Thread-Objekt während der gesamten Ausführung der Thread-Funktion erhalten bleibt; dies Design erscheint auch sinnvoll -

- - -
-
- - - - - - -

- der bisherige Lumiera-Threadwrapper hat zwar ein Thread-Handle gehalten, das aber letztlich beim Threadpool registriert war. Daher war das Thread-Objekt selber im Grunde verzichtbar (es sei denn, man hat davon abgeleitet und dort weitere Felder untergebracht) -

- - -
-
- - - -
- - - - - - - - @@ -80190,6 +80603,12 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + @@ -80589,9 +81008,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

...denn die Fehler sind nicht Teil des Tests, sondern »könnten« nur auftregen (wenn irgendwo in der Applikation was faul ist) @@ -80932,9 +81349,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

CHECK: session-command-function-test.cpp:425: thread_1: perform_massivelyParallel: (testCommandState - prevState == Time(expectedOffset)) @@ -80978,9 +81393,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

würde es nicht genügen, @@ -81004,9 +81417,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

...das bedeutet, es wird erst zur Laufzeit bestimmt, in welcher Reihenfolge die Counter für die Typen alloziert werden; damit kommt es zu Beginn zu einer aggressiven contention auf slot<X>. Einschränkung: in dieser Form wirkt dieser Test nur beim ersten Lauf innerhalb einer Programm-Instanz, weil danach die Slots belegt sind. In der Praxis stellt das keine Einschränkung dar @@ -81016,9 +81427,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

...denn es muß eine hohe Wahrscheinlichkeit geben, daß gleichzeitig zwei Threads auf den gleichen Counter zugreifen ⟹ es muß deutlich mehr Threads geben als counter. Das ist aber schwierig, weil die Zahl der Cores beschränkt ist. Hier hilft nur (a) sehr viel zu viele Threads verwenden und (b) diese lang laufen lassen. @@ -81039,9 +81448,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

das heißt ich muß keinen abstrakten Typ mehr konstruieren und daraus abgeleitete Dummy<i>, weil letzten Endes nur zwei Operationen notwendig sind: den Zähler inkrementieren und am Ende den Zählerstand auslesen @@ -81065,9 +81472,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

das gilt aber nur, wenn nicht bereits summAllCounters() aufgerufen wurde! @@ -81156,17 +81561,20 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + + - - + + + + - + @@ -81306,9 +81714,10 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + - + @@ -81340,6 +81749,25 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + + + + + + + + + + + +
@@ -81359,7 +81787,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + +