From 2c18c39c18a4b6db8f4c0dfda01a81561529542d Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 28 Sep 2023 01:09:07 +0200 Subject: [PATCH] Library: complete the Thread-joining policy after all this groundwork, implementing the invocation, capturing and hand-over of results is simple, and the thread-wrapper classes became fairly understandable. --- src/lib/result.hpp | 10 +- src/lib/thread.cpp | 4 +- src/lib/thread.hpp | 193 ++++++++++----- tests/library/sync-classlock-test.cpp | 22 +- tests/library/sync-locking-test.cpp | 2 +- tests/library/sync-waiting-test.cpp | 2 +- wiki/thinkPad.ichthyo.mm | 333 +++++++++++++------------- 7 files changed, 321 insertions(+), 245 deletions(-) diff --git a/src/lib/result.hpp b/src/lib/result.hpp index 3ef4f3df9..7787d5160 100644 --- a/src/lib/result.hpp +++ b/src/lib/result.hpp @@ -73,7 +73,7 @@ namespace lib { inline auto failsafeInvoke (std::exception_ptr& capturedFailure ,FUN&& callable - ,ARGS&& ...args) + ,ARGS&& ...args) noexcept { using Res = std::invoke_result_t; try { @@ -129,7 +129,7 @@ namespace lib { /** invoke a _callable_ and mark success or failure */ template>> - Result (FUN&& callable, ARGS&& ...args) + Result (FUN&& callable, ARGS&& ...args) noexcept : failure_{} { failsafeInvoke (failure_ @@ -180,7 +180,7 @@ namespace lib { /** invoke a _callable_ and capture result in one shot */ template>> - Result (FUN&& callable, ARGS&& ...args) + Result (FUN&& callable, ARGS&& ...args) noexcept : Result{true} , value_{failsafeInvoke (failure_ ,std::forward (callable) @@ -222,11 +222,11 @@ namespace lib { } }; - /** deduction guard: allow _perfect forwarding_ of a any result into the ctor call. */ + /** deduction guide: allow _perfect forwarding_ of a any result into the ctor call. */ template>> Result (VAL&&) -> Result; - /** deduction guard: find out about result value to capture from a generic callable. */ + /** deduction guide: find out about result value to capture from a generic callable. */ template Result (FUN&&, ARGS&&...) -> Result>; diff --git a/src/lib/thread.cpp b/src/lib/thread.cpp index bd025f0bd..8c18db741 100644 --- a/src/lib/thread.cpp +++ b/src/lib/thread.cpp @@ -48,6 +48,8 @@ namespace lib { namespace thread{ namespace { + const auto SHUTDOWN_GRACE_PERIOD = 20ms; + string lifecycleMsg (Literal phase, string threadID) { @@ -95,7 +97,7 @@ namespace thread{ try { auto start = steady_clock::now(); while (threadImpl_.joinable() - and steady_clock::now () - start < 20ms + and steady_clock::now () - start < SHUTDOWN_GRACE_PERIOD ) std::this_thread::yield(); } diff --git a/src/lib/thread.hpp b/src/lib/thread.hpp index 0c570d2de..81821e896 100644 --- a/src/lib/thread.hpp +++ b/src/lib/thread.hpp @@ -24,15 +24,16 @@ /** @file thread.hpp ** Convenience front-end to simplify and codify basic thread handling. - ** While the implementation of threading and concurrency support is based on the C++ - ** standard library, using in-project wrappers as front-end allows to codify some preferences - ** and provide simplifications for the prevalent use case. Notably, threads which must be - ** _joined_ are qualified as special case, while the standard case will just `detach()` - ** at thread end. The main-level of each thread catches exceptions, which are typically - ** ignored to keep the application running. Moreover, similar convenience wrappers are - ** provided to implement [N-fold synchronisation](\ref lib::SyncBarrier) and to organise - ** global locking and waiting in accordance with the _Object Monitor_ pattern. Together, - ** these aim at packaging concurrency facilities into self-contained RAII-style objects. + ** While the implementation of threading and concurrency support is based on the + ** C++ standard library, using in-project wrappers as front-end allows to codify some + ** references and provide simplifications for the prevalent use case. Notably, threads + ** which must be _joined_ are qualified as special case, while the standard case will + ** just `detach()` at thread end. The main-level of each thread catches exceptions, which + ** are typically ignored to keep the application running. Moreover, similar convenience + ** wrappers are provided to implement [N-fold synchronisation](\ref lib::SyncBarrier) + ** and to organise global [locking and waiting](\ref lib::Sync) in accordance with the + ** _Object Monitor_ pattern. In concert, these allow to package concurrency facilities + ** into self-contained RAII-style objects. ** ** # Usage ** Based on experience, there seem to be two fundamentally different usage patterns for @@ -43,19 +44,21 @@ ** ** The »just launch it« scheme is considered the default and embodied into lib::Thread. ** Immediately launched on construction using the given _Invokable Functor_ and binding arguments, - ** it is not meant to be managed further, beyond possibly detecting the live-ness state through - ** `bool`-check. Exceptions propagating to top level within the new thread will be catched and - ** ignored, terminating and discarding the thread. Note however, since especially derived + ** such a thread is not meant to be managed further, beyond possibly detecting the live-ness state + ** through `bool`-check. Exceptions propagating to top level within the new thread will be coughed + ** and ignored, terminating and discarding the thread. Note however, since especially derived ** classes can be used to create a safe anchor and working space for the launched operations, - ** it must be avoided to discard the Thread object while still operational; as a matter of + ** it must be avoided to destroy the Thread object while still operational; as a matter of ** design, it should be assured the instance object outlives the enclosed chain of activity. ** 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. + ** the `join()` operation. This operation returns a [»Either« wrapper](\ref lib::Result), + ** to transport the return value and possible exceptions from the thread function to the + ** caller. 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 @@ -73,7 +76,7 @@ ** in cases where a race could be critical, additional means must be implemented; a ** possible solution would be to use a [N-fold synchronisation barrier](\ref lib::SyncBarrier) ** explicitly, or otherwise to ensure there is sufficient delay in the starting thread function. - ** + ** ** @remarks Historical design evolution: ** - Lumiera offered simplified convenience wrappers long before a similar design ** became part of the C++14 standard. These featured the distinction in join-able or @@ -92,8 +95,8 @@ ** and paved the way for switch-over to the threading support meanwhile part of the ** C++ standard library. Design and semantics were retained, while implemented ** using modern features, notably the new _Atomics_ synchronisation framework. + ** ** [syncs-with definition] : https://en.cppreference.com/w/cpp/atomic/memory_order#Synchronizes_with - ** @todo WIP 9/23 about to be replaced by a thin wrapper on top of C++17 threads ///////////////////////TICKET #1279 : consolidate to C++17 features */ @@ -105,6 +108,7 @@ #include "lib/nocopy.hpp" #include "include/logging.h" #include "lib/meta/function.hpp" +#include "lib/result.hpp" #include #include @@ -122,6 +126,10 @@ namespace lib { namespace thread {// Thread-wrapper base implementation... + /** @internal wraps the C++ thread handle + * and provides some implementation details, + * which are then combined by the _policy template_ + */ struct ThreadWrapper : util::MoveOnly { @@ -153,12 +161,37 @@ namespace lib { }; - template - struct PolicyTODO + + /** + * Thread Lifecycle Policy: + * - launch thread without further control + * - errors in thread function will only be logged + * - thread detaches before terminating + * - »grace period« for thread to terminate on shutdown + */ + template + struct PolicyLaunchOnly : BAS { using BAS::BAS; + template + void + perform_thread_function(FUN&& callable, ARGS&& ...args) + { + try { + // execute the actual operation in this new thread + std::invoke (std::forward (callable), std::forward (args)...); + } + ERROR_LOG_AND_IGNORE (thread, "Thread function") + } + + void + handle_end_of_thread() + { + BAS::threadImpl_.detach(); + } + void handle_thread_still_running() { @@ -166,29 +199,69 @@ namespace lib { } }; + /** - * Policy-based configuration of thread lifecycle + * Thread Lifecycle Policy: + * - thread with the ability to publish results + * - return value from the thread function will be stored + * - errors in thread function will likewise be captured and retained + * - thread *must* be joined after termination of the thread function + * @warning unjoined thread on dtor call will be a fatal error (std::terminate) */ - template class POL> - class ThreadLifecycle - : protected POL + template + struct PolicyResultJoin + : BAS { - using Policy = POL; + using BAS::BAS; + + /** Wrapper to capture a success/failure indicator and possibly a computation result */ + lib::Result result_{error::Logic{"Thread still running; need to join() first."}}; + template void - main (FUN&& threadFunction, ARGS&& ...args) + perform_thread_function(FUN&& callable, ARGS&& ...args) + { + static_assert (std::__or_ + ,std::is_constructible>>()); + + // perform the given operation (failsafe) within this thread and capture result... + result_ = std::move ( + lib::Result{std::forward(callable) + ,std::forward(args)...}); + } + + void + handle_end_of_thread() + { + /* do nothing -- thread must be joined manually */; + } + + void + handle_thread_still_running() + { + ALERT (thread, "Thread '%s' was not joined. Abort.", BAS::threadID_.c_str()); + } + }; + + + /** + * Policy-based configuration of thread lifecycle + */ + template class POL, typename RES =void> + class ThreadLifecycle + : protected POL + { + using Policy = POL; + + template + void + invokeThreadFunction (ARGS&& ...args) { Policy::markThreadStart(); - try { - // execute the actual operation in this new thread - std::invoke (std::forward (threadFunction), std::forward (args)...); - } - ERROR_LOG_AND_IGNORE (thread, "Thread function") - // + Policy::perform_thread_function (std::forward (args)...); Policy::markThreadEnd(); -// if (autoTerm) - Policy::threadImpl_.detach(); + Policy::handle_end_of_thread(); } @@ -216,31 +289,28 @@ namespace lib { /** Create a new thread to execute the given operation. * The new thread starts up synchronously, can't be cancelled and it can't be joined. * @param threadID human readable descriptor to identify the thread for diagnostics - * @param logging_flag NoBug flag to receive diagnostics regarding the new thread - * @param operation a functor holding the code to execute within the new thread. - * Any function-like entity with signature `void(void)` is acceptable. - * @warning The operation functor will be forwarded to create a copy residing - * on the stack of the new thread; thus it can be transient, however - * anything referred through a lambda closure here must stay alive - * until the new thread terminates. + * @param threadFunction a functor holding the code to execute within the new thread. + * Any function-like entity or callable is acceptable; arguments can be given. + * @warning The operation functor and all arguments will be copied into the new thread. + * The return from this constructor _syncs-with_ the launch of the operation. */ template ThreadLifecycle (string const& threadID, FUN&& threadFunction, ARGS&& ...args) : Policy{threadID - , &ThreadLifecycle::main, this + , &ThreadLifecycle::invokeThreadFunction, this , std::forward (threadFunction) , std::forward (args)... } { } - }; }//(End)base implementation. + /************************************************************************//** - * A thin convenience wrapper to simplify thread-handling. The implementation - * is backed by the C++ standard library. + * A thin convenience wrapper to simplify thread-handling. + * The implementation is backed by the C++ standard library. * Using this wrapper... * - removes the need to join() threads, catches and ignores exceptions. * - allows to bind to various kinds of functions including member functions @@ -250,7 +320,7 @@ namespace lib { * `std::terminate` afterwards, should the thread still be active then. */ class Thread - : public thread::ThreadLifecycle + : public thread::ThreadLifecycle { public: @@ -260,8 +330,6 @@ namespace lib { - - /************************************************************************//** * Variant of the [standard case](\ref Thread), requiring to wait and `join()` * on the termination of this thread. Useful to collect results calculated @@ -271,22 +339,39 @@ namespace lib { * @warning Thread must be joined prior to destructor invocation, otherwise * the application is shut down immediately via `std::terminate`. */ + template class ThreadJoinable - : public thread::ThreadLifecycle + : public thread::ThreadLifecycle { - public: - using ThreadLifecycle::ThreadLifecycle; + using Impl = thread::ThreadLifecycle; - /** put the caller into a blocking wait until this thread has terminated */ - void + public: + using Impl::Impl; + + /** + * put the caller into a blocking wait until this thread has terminated + * @return intermediary token signalling either success or failure. + * The caller can find out by invoking `isValid()` or `maybeThrow()` + * on this result token. Moreover, if the _thread function_ yields a + * result value, this value is copied into the token and can be retrieved + * either by type conversion, or with `get()`, `value_or(default)` + * or even with an alternative producer `or_else(λ)`. + */ + lib::Result join () { - if (not threadImpl_.joinable()) + if (not Impl::threadImpl_.joinable()) throw lumiera::error::Logic ("joining on an already terminated thread"); - threadImpl_.join(); + Impl::threadImpl_.join(); + + return Impl::result_; } }; + + /** deduction guide: find out about result value to capture from a generic callable. */ + template + ThreadJoinable (string const&, FUN&&, ARGS&&...) -> ThreadJoinable>; diff --git a/tests/library/sync-classlock-test.cpp b/tests/library/sync-classlock-test.cpp index 6e682b1b9..4cbbcf426 100644 --- a/tests/library/sync-classlock-test.cpp +++ b/tests/library/sync-classlock-test.cpp @@ -65,24 +65,24 @@ namespace test { { int contended = 0; - using Threads = lib::ScopedCollection; + using Threads = lib::ScopedCollection>; // Start a bunch of threads with random access pattern Threads threads{NUM_THREADS, [&](Threads::ElementHolder& storage) { - storage.create ("Sync-ClassLock stress test" - ,[&]{ - for (uint i=0; i> ("Sync-ClassLock stress test" + ,[&]{ + for (uint i=0; i guard; - ++contended; + uint delay = rand() % 10; + usleep (delay); + { + ClassLock guard; + ++contended; + } } - } - }); + }); } }; diff --git a/tests/library/sync-locking-test.cpp b/tests/library/sync-locking-test.cpp index e8fdd0798..ffb7d3bb2 100644 --- a/tests/library/sync-locking-test.cpp +++ b/tests/library/sync-locking-test.cpp @@ -126,7 +126,7 @@ namespace test{ */ class HavocThread { - ThreadJoinable thread_; + ThreadJoinable<> thread_; void doIt () diff --git a/tests/library/sync-waiting-test.cpp b/tests/library/sync-waiting-test.cpp index ca614f8d7..0e82914a3 100644 --- a/tests/library/sync-waiting-test.cpp +++ b/tests/library/sync-waiting-test.cpp @@ -156,7 +156,7 @@ namespace test{ void waitPingPong (Token& tok) { - typedef ThreadJoinable Thread; //////////////////////////////////////WIP + typedef ThreadJoinable<> Thread; //////////////////////////////////////WIP Thread ping ("SyncWaiting ping", bind (&Token::getIt, &tok)); Thread pong ("SyncWaiting pong", bind (&Token::getIt, &tok)); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index dbf540dab..58af91f14 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -16961,7 +16961,8 @@ - + + @@ -54038,9 +54039,7 @@ - - - +

Diff-basiertes Δ-Binding @@ -57258,6 +57257,42 @@ + + + + + + + + + + + + + + + + + +

+ das bedeutet: man kann alles, was per std::invoke aufrufbar ist, in eine Ergebnis-Repräsentation materialisieren. Im Besonderen nivelliert das automatisch (zur compile-Zeit) die Unterscheidung void / Rückgabewert. Jedwede Exception beim Aufruf wird gefangen +

+ +
+
+ + + + + + + + + + + + + @@ -78950,9 +78985,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Um das klar zu sagen: zwar hat Christian ein Verbot von »join« angedacht, aber grade in dieser Frage war und bin ich absolut einer Meinung mit ihm. Tatsächlich war Christian sogar zurückhaltender („man sollte es deprecaten“) — und das hat mich  dann auf die Idee gebracht, einen separaten »Joinable«-Wrapper zu schreiben. Und jetzt, viele Jahre später stehe ich an dem Punkt, daß es kein absehbares Usage-Pattern gibt, und mein Design von damals ungerchtfertigt erscheint. @@ -79123,9 +79156,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

...trotzdem war ich überrascht, um wie viel langsamer sie ist; das kann ich mir eigentlich nur dadurch erklären, daß die Threads in einen Schlafzustand versetzt werden, ggfs auch bereits schon beim Versuch, die exclusive Zone zu betreten. Möglicherweise dauert es auch grundsätzlich länger, bis ein schlafender Thread überhaupt wieder aufgeweckt wird. Die Progression scheint allerdings linear in der Zahl der Threads zu sein, während die Atomic-yield-Implementierung etwas überproportional langsamer wird. Das ist jetzt aber mehr Intuition, denn jenseits von 8 Threads gibt es ja zunehmend Stau im OS-Scheduler @@ -79137,9 +79168,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Zum einen handelt es sich um bekannte Erfahrungswerte, zumindest für die Fälle mit wenigen Threads: daß zwei zusammen gestartete Threads um bis zu 0.5µs auseinanderlaufen, ist erwartbar, wesentlich mehr aber nicht ohne Weiteres. Und dann läßt es sich bestätigen, indem die Implementierung versuchsweise auf busy-wait umgestellt wird ⟹ für kleine Anzahl Threads bleiben die Meßwerte nahezu unverändert (sie sind minimal schlechter, aber das System geht auch in Vollast). Das bedeutet: die beobachteten Werte stellen bereits nahezu optimales Verhalten dar, für kleine Anzahl Threads. @@ -79236,9 +79265,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - + @@ -79247,9 +79276,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

YAGNI. @@ -79270,9 +79297,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

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). @@ -79289,9 +79314,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

explizit eine lib::SyncBarrier in der Implementierung einbinden @@ -79314,13 +79337,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + - - - +

...damit man auch zusammengesetzte/formatierte Werte bauen kann @@ -79338,15 +79359,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + - - - +

std::thread::native_handle() ⟼ liefert hier pthread_t @@ -79368,9 +79387,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

sollte dann den this-Typ extrahieren und den this-Ptr automatisch injizieren @@ -79380,8 +79397,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -79392,9 +79409,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

der Name ist nicht besonders klar @@ -79408,9 +79423,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Die hatte ich eingebaut, um für spezialisierte abgeleitete Klassen doch noch erweiterte Zustandsübergänge zu ermöglichen @@ -79425,13 +79438,11 @@ 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. @@ -79441,9 +79452,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

...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 @@ -79453,25 +79462,24 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

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) @@ -79485,7 +79493,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -79504,15 +79513,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - - - +

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“ @@ -79522,9 +79529,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

heute bevorzugt man für ähnliche Anforderungen das Future / Promise - Konstrukt @@ -79535,9 +79540,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

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 @@ -79549,7 +79552,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -79561,9 +79564,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Knoten durchhauen! @@ -79584,21 +79585,25 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + + + + - + + + + - - - +

und kann zugleich jedwede Exception halten @@ -79610,9 +79615,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

und ansonsten aber left-biassed sein @@ -79622,9 +79625,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

incl perfect forwarding @@ -79638,8 +79639,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + @@ -79654,11 +79656,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - - - +

Es wäre zwar so irgendwie hinzubekommen — aber weit entfernt von einem guten Design. @@ -79667,14 +79667,12 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
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) @@ -79684,9 +79682,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

die Code-Anordnung hat keinen Flow @@ -79697,9 +79693,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Ich habe doch selber oben diese Prinzipien formuliert: eine Library-Lösung sollte nicht mutwillig beschränken, sondern eine sinnvolle Gliederung anbieten... @@ -79710,17 +79704,16 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - - - +

also vielleicht doch Policy-based-Design?

+ @@ -79730,9 +79723,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

⟹ wenn überhaupt, müßte die Policy einen mittleren Binding-Layer bilden @@ -79753,12 +79744,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + - - + + @@ -79779,6 +79771,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ @@ -79788,15 +79781,35 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + +

+ 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...> @@ -79810,11 +79823,18 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + + - - + + + + + + + @@ -79940,9 +79960,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

  • @@ -79975,9 +79993,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - +

    Nanos wären die natürliche Skala für moderne PCs @@ -80017,9 +80033,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - +

    für den BlockFlow-Test habe ich das definitiv gebraucht, um damit eine »Zeitachse« zu konstruieren; und auch für multithreaded-Tests ist das innerhalb des einzelnen Thread durchaus sinnvoll (⟹ siehe SyncBarrierPerformance_test) @@ -80055,9 +80069,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - +

    es ist ja ein einziger Zufallszahlengenerator, und es wäre eine schlechte Idee, wenn die Stdlib das nicht gegen concurrency schützen würde @@ -80115,9 +80127,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - +

    Messungen(Release-Build) @@ -80154,9 +80164,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - +

    • @@ -80174,9 +80182,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      - - - +

      ...die führen dann nochmal zu um den Faktor 10 größeren Werten (was mit meiner Erfahrung konsistent ist).
      Daher erscheint die aktuelle Lösung als optimal: wir zwingen den Optimiser, die Schleife auszuführen, weil ein Wert berechnet wird; dieser greift aber nur auf eine Variable in der Klasse zu, und muß nicht atomar, volatil oder synchronisiert sein. Mit diesem Setup kann man also auch den Einfluß von Atomic-Zugriffen noch gut messen @@ -80191,9 +80197,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      - - - +

      wir messen, wie lange ein Thread im Durchschnitt baucht, bis er sich via SyncBarrier mit den anderen Partner-Threads synchronisiert hat. Dieser Wert ist nicht deterministisch, da die zeitliche Lage der Threads zueinander nicht deterministisch ist. Wir können aber auch nicht anders messen, da der Thread typischerweise in der sync()-Funktion blockt. @@ -80203,9 +80207,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      - - - +

      ⟹ wir beobachten die Barriere bei ihrer bestimmungsgemäßen Arbeit @@ -80216,9 +80218,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      - - - +

      ⟹ wir bekommen so nicht den Implementierungs-Overhead  zu fassen @@ -89230,38 +89230,31 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      + - - - +

      Wenn man eine Klasse C<T> hat, und einen Konstruktur definiert, der ein T&& - Argument nimmt, dann ist das T bereits durch den Klassen-Templateparameter festgelegt; es kommt in diesem Fall nicht zu einem reference collapsing, sondern es handelt sich einfach um eine rvalue-Referenz T&&. Symptom "can not bind rvalue reference to lvalue"

      - -
      +
      - - - +

      ...denn in diesem Fall würde es sich zwar um eine »universelle Referenz« handeln, aber der Zusammenhang zum Klassen-Template-Parameter ist nicht klar

      - -
      +
      - - - +
      • @@ -89287,14 +89280,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
          Result (VAL&&) -> Result<VAL>;

        - - + - - - +

        Wenn ich die Beschreibung in CPPReference.com richtig verstehe, dann würde ohne expliziten deduction guide folgender guide automatisch generiert: @@ -89381,8 +89371,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
        Was ich nicht verstehe: der gemäß Regeln auto-generierte Guide sieht exakt genauso aus, wie der Guide, den man manuell definieren muß, damit es funktioniert. Warum wird dann hier ein anderer Typ deduziert? Ich habe Experimente gemacht: ohne den manuellen deduction Guide ergibt sich für einen <initializer> ≔ int&  ⟼ ein deduzierter Parameter X = int  (was dann natürlich scheitert)

        - -
        +