Lumiera  0.pre.03
»edit your freedom«
thread.hpp File Reference

Go to the source code of this file.

Description

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 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 and to organise global locking and waiting 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 thread-like entities: In most cases, they are just launched to participate in interactions elsewhere defined. However, sometimes dedicated sub-processing is established and supervised, finally to join results. And while the underlying implementation supports both usage styles, a decision was made to reflect this dichotomy by casting two largely distinct front-ends.

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, 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 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 lib::ThreadJoinable is provided, exposing the join() operation. This operation returns a »Either« wrapper, 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.

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 the return from join(). According to the [syncs-with definition], this implies the happens before relation and thus precludes a data race. In practice thus

  • the new thread function can access all data defined prior to ctor invocation
  • the caller of join() is guaranteed to see all effects of the terminated thread. Note however, that these guarantees do not extend into the initialisations performed in a derived class's constructor, which start only after leaving the ctor of Thread. So in theory there is a possible race between the extended setup in derived classes, and the use of these facilities from within the thread function. In practice the new thread, while already marked as live, still must be scheduled by the OS to commence, which does not completely remove the possibility of undefined behaviour however. So 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 explicitly, or otherwise to ensure there is sufficient delay in the starting thread function.

Caveat

While these thread-wrapper building blocks aim at packaging the complexity away, there is the danger to miss a potential race, which is inherent with starting threads: the operation in the new thread contends with any initialisation done after launching the thread. Even though encapsulating complex concurrent logic into an opaque component, as built on top of the thread-wrappers, is highly desirable from a code sanity angle — it is dangerously tempting to package self-contained data initialisation into a subclass, leading to the kind of undefined behaviour, which „can never happen“ under normal circumstances. Even while the OS scheduler typically adds an latency of at least 100µs to the start of the new thread function, initialising anything (even subclass data members) after creating the thread-wrapper instance is undefined behaviour. As a remedy

  • it should be considered to put the thread-warpper into a member (instead of inheriting)
  • an explicit lib::SyncBarrier can be added, to ensure the thread-function touches any extended facilities only after the initialisation is complete (as a downside, note that any hard synchronisation adds a possibility for deadlock).
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 detached threads, the ability to define the thread main-entry as functor, and a two-fold barrier between starter and new thread, which could also be used to define a second custom synchronisation point. A similar setup with wrappers was provided for locking, exposed in the form of the Object Monitor pattern.
  • The original Render Engine design called for an active thread-pool, which was part of a invoker service located in Vault layer; the thread-wrapper could only be used in conjunction with this pool, re-using detached and terminated threads. All features where implemented in plain-C on top of POSIX, using Mutexes and Condition Variables.
  • In 2023, when actually heading towards integration of the Render Engine, in-depth analysis showed that active dispatch into a thread pool would in fact complicate the scheduling of Render-Activities — leading to a design change towards pull of work tasks by competing active workers. This obsoleted the Thread-pool service 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

Definition in file thread.hpp.

#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "include/logging.h"
#include "lib/meta/trait.hpp"
#include "lib/meta/function.hpp"
#include "lib/format-util.hpp"
#include "lib/result.hpp"
#include <utility>
#include <thread>
#include <string>
#include <tuple>

Classes

struct  ThreadLifecycle< POL, RES >::Launch
 Configuration builder to define the operation running within the thread, and possibly configure further details, depending on the actual Policy used. More...
 
struct  PolicyLaunchOnly< BAS, typename >
 Thread Lifecycle Policy: More...
 
struct  PolicyLifecycleHook< BAS, TAR >
 Thread Lifecycle Policy Extension: invoke user-provided callbacks from within thread lifecycle. More...
 
struct  PolicyResultJoin< BAS, RES >
 Thread Lifecycle Policy: More...
 
class  Thread
 A thin convenience wrapper to simplify thread-handling. More...
 
class  ThreadHookable
 Extended variant of the standard case, allowing to install callbacks (hook functions) to be invoked during thread lifecycle: More...
 
class  ThreadJoinable< RES >
 Variant of the standard case, requiring to wait and join() on the termination of this thread. More...
 
class  ThreadLifecycle< POL, RES >
 Policy-based configuration of thread lifecycle. More...
 
struct  ThreadWrapper
 

Functions

template<class TAR = ThreadHookable>
void launchDetached (ThreadHookable::Launch &&launchBuilder)
 Launch an autonomous self-managing thread (and forget about it). More...
 
template<class TAR = ThreadHookable, typename... INVO>
void launchDetached (string const &threadID, INVO &&...args)
 Launch an autonomous self-managing thread (and forget about it). More...
 
template<class TAR , typename... ARGS>
void launchDetached (string const &threadID, void(TAR::*memFun)(ARGS...), ARGS ...args)
 Special variant bind a member function of the subclass into the autonomous thread.
 
template<class TAR , typename... ARGS>
void launchDetached (void(TAR::*memFun)(ARGS...), ARGS ...args)
 Special variant without explicitly given thread-ID.
 
std::string sanitise (string const &org)
 produce an identifier based on the given string. More...
 
template<typename FUN , typename... ARGS>
 ThreadJoinable (string const &, FUN &&, ARGS &&...) -> ThreadJoinable< std::invoke_result_t< FUN, ARGS... >>
 deduction guide: find out about result value to capture from a generic callable. More...
 

Namespaces

 lib
 Implementation namespace for support and library code.
 

Function Documentation

◆ sanitise()

string sanitise ( string const &  org)

produce an identifier based on the given string.

remove non-standard-chars, reduce sequences of punctuation and whitespace to single underscores. The sanitised string will start with an alphanumeric character.

Example Conversions
   "Word"                             --> "Word"
   "a Sentence"                       --> "a_Sentence"
   "trailing Withespace  \t \n"       --> "trailing_Withespace"
   "with    a   lot  \nof Whitespace" --> "with_a_lot_of_Whitespace"
   "@with\".\'much ($punctuation)[]!" --> "@with.much_($punctuation)"
   "§&Ω%€  leading garbage"           --> "leading_garbage"
   "mixed    Ω   garbage"             --> "mixed_garbage"
   "Bääääh!!"                         --> "Bh"
See also
UtilSanitizedIdentifier_test
lib::meta::sanitisedSymbol()

Definition at line 56 of file util.cpp.

References util::isValid(), and util::sanitise().

Referenced by util::sanitise().

+ Here is the call graph for this function:
+ Here is the caller graph for this function: