ALib C++ Library
Library Version: 2412 R0
Documentation generated by doxygen
Loading...
Searching...
No Matches
ALib Module Threads - Programmer's Manual

1. Introduction

The fundamental two topics that a class library dedicated "multithreaded programming" adress are:

  1. Launching and managing threads, and
  2. protecting data against corruption caused by uncontrolled concurrent access (racing conditions on critical sections).

And this is what this small foundational ALib Module provides. All types rely on the C++ standard libraries for thread management, and in fact are not much more than wrappers. What is added is mainly debug-features, which disappear in release compilations.
The classes are quite aligned with the types found in the class libraries of the Java language in their early versions.

Note
ALib provides sophistic threading support is provided with a higher-leve module called ALib ThreadModel.

Besides the provision of these two fundamental requirements, this module as acts as a code selector for multithreading support of ALib in general. This is explained in the next section.

1.1 The Special Role Of This Module

One of ALib's design targets is modularity, and a user is free to compile a reduced library, containing only a subset of the modules available, picking the things a user is interested in and skipping modules they are not.

With this module ALib Threads it is a little different. Therefore, we give a red signal for this rule:

Attention
As an exception to the rule, module ALib Threads has to be chosen to be included in an ALib Distribution, if a project uses multithreading. Thus, the choice is independent of a user's wish to use the types found in this module! In other words: A user might use any other library to create and manage threads. Nevertheless, as soon as parallel calls into ALib are made, this module has to be included.

In the case this module is used within a very limited ALib Distribution, please note the explanations found in the ALib Programmer's Manual, chapter 4.2 Bootstrapping Non-Camp Modules. In short, it has to be ensured that namespace functions alib::threads::Bootstrap and alib::threads::Bootstrap are duly called with the start, respectively at the end of an application, which is ensured if the guidelines given in the chapter linked above are followed.

1.2 Threading-Agnostic Software

The term used in the headline of this section needs to be explained: As a general-purpose library, ALib aims to support both use with single threaded and multithreaded environments. To achieve this, three guidelines are followed:

  1. With the inclusion of this module, critical sections become internally protected.
  2. Only those sections that the using code cannot protect by himself have built-in protection.
    Note
    As an example, container class HashTable is not protected because a using code can protect interfacing with an instance by itself. On the other hand, accessing the default formatter instance is protected because it may be used internally with some higher level functionality, for example with Exception::Format.
  3. With the exclusion of this module, all protection is pruned to maximize execution performance.

To achieve this, some sort of "cooperation" between the core of ALib and this module is necessary. This cooperation means: In the absence of ALib Threads, still some skeleton of types and macros need to exist. As samples for this take macro ALIB_LOCK_WITH or debug-type alib::lang::DbgCriticalSections. As seen, the latter type is placed in core namespace alib::lang and in the absence of this module, remains there as an empty type. Usages of it are optimized out.

"Cooperation" here is a positive wording. Negatively expressed, it could be said that "the isolation rules were broken". The benefit of this design decision is that all other code can use the macros and types independent from the ALib Distribution. This avoids code clutter in ALib as well as in custom code that opts in to using these features for themselves.

1.3 Pruning

This section lists entities placed in alib::lang instead of alib::threads and macros that are likewise always available regardless if this module is included in an ALib Distribution or not. If it is not included, the macros are defined "empty", so that their use is pruned.

Please consult the reference documentation of the listed entities for more information.



1.4 Assertions With Debug-Builds

1.4.1 Background Considerations

The few fundamental types offered by this module provide typical assertions in debug-builds, as found with similar types provided elsewhere. For example, if a Lock is destructed, it is asserted that it is not locked.

Besides those, ALib proposes a rather uncommon type of assertion mechanic. This is implemented with class lang::DbgCriticalSections (explicitly located not in this module) and those preprocessor macros listed in the previous section whose name starts with ALIB_DCS.
The approach taken is simply to mark certain code segments as being critical. Critical sections vary by use case, making it impossible for "lower-level" code entities to consistently judge their presence.

Let us look at the classic sample: A value is

  1. read,
  2. changed, and
  3. written back.

If executed in parallel threads, these three steps comprise a critical section. Now, if the value resides in type ContainerType, formally the instructions are:

  1. value= ContainerType.Read(key)
  2. value.Change
  3. ContainerType.Write(key, value).

At the level of the container code, namely inside methods Read and Write, it cannot be decided whether this is a critical section or not: If Write is not included in an operation, it is not, while if it is, both operations are to be included in the critical section.

Therefore, putting locking mechanisms into container types mitigates only the most simple cases. Only the using code is able to lock all necessary sequences and can do this as a whole.

Note
A prominent sample when indeed locking was performed on container type level, is found in the initial version of the Java Class Library. Here, types like util.Vector and util.Hashtable, synchronized every method call, aiming to ensure thread safety. As just explained, this approach can be considered flawed, because:
  1. Unnecessary performance costs are imposed, and
  2. it provided an illusion of safety.
This highlights the importance of context-aware synchronization rather being able to relying on built-in, method-level locking. The C++ standard library does not perform locks, with only a very few exceptions. For example, allocation of heap memory is synchronized with methods std::malloc and std::free.

From this perspective it could be assumed that likewise any debug-assertion concerning the protection of a critical section cannot be made in low-level code. To stay with the sample, it could be assumed that it cannot be made with methods ContainerType.Read and Write.

But it can! For that turn, two things are necessary:

  1. It must be allowed that not all cases can be fetched:
    This condition is met, because in the case of assertions, if at least some cases are fetched, it is better than none.
  2. Operations have to be separated into writing and read-only-operations:
    While this sounds a little like turning the use case to one that fits C++ 17 type
    std::shared_mutex , namely a two-level access strategy, it is not: Only "raising assertions" is turned into such a use case - but not the use case of locking!

1.4.2 Asserting Critical Sections' Entry And Exit

With this theory discussed, type DbgCriticalSections can now be quickly explained.

  • A combined approach of asserting sections that might get protected by standard locks, recursive locks, and shared locks is implemented with this single type.
  • The type usually is inherited publicly by types that are typical candidates to impose critical sections and multithreaded access. For example, within ALib among such types are allocators and
    container types.
    If compiler symbol ALIB_DEBUG_CRITICAL_SECTIONS is set, these types conditionally derive from DbgCriticalSections or add a corresponding member.
  • Acquirement, normal or shared, is always recursive with this debug-type. Otherwise, its use would become too complicated, because, for example, the container types often call its functions in a nested fashion.

    Furthermore, shared acquirement is allowed if the same thread already performed full acquirement. The opposite is asserted: It is not allowed to perform full acquirement when already shared acquisition was made before.
  • The interface functions of types equipped with an instance of DbgCriticalSections acquire this instance in either normal or shared mode.
    For example, all interface methods of the allocator-types assert for full (non-shared) access.
    The write methods of container types likewise check for full access, while their read methods check for shared access only.
  • If a type inherited from DbgCriticalSections, the easiest way to acquire, is to use macros ALIB_DCS or ALIB_DCS_SHARED.
    If DbgCriticalSections is implemented as a member, alternative macros ALIB_DCS_WITH and ALIB_DCS_SHARED_WITH are to be used.
    These macros create an anonymous instance of class Owner (respectively OwnerShared), which assures the release of the section, even if exceptions are thrown.
  • Alternative macros, which do not rely on the automation mechanics of class Owner and it siblings, are given with:
  • Finally, if module ALib Threads is not included in an ALib Distribution, then with debug-compilations, the macros above
    assert single threaded use of ALib.

The output written with assertions should be 'clickable' inside a users' IDE. The default output string is optimized for JetBrains CLion and can be changed by manipulating the static field member DbgCriticalSections::ASSERTION_FORMAT.

1.4.3 Asserting Critical Section Locks

As discussed in the previous chapters, only the using code entities have "knowledge" about what are the real sequences which have to be executed atomically by threads. Therefore, the captured assertions in lower-level code entities can fetch only a subset of the compromising cases.

It is clear that the underlying code even more has no knowledge about which instance of a mutex has to be locked when entering a critical section. Class DbgCriticalSections allows receiving this knowledge. This is done using AssociatedLock, which is a virtual struct
that allows answering the question about whether or not "something" is acquired. With compiler symbol ALIB_DEBUG_CRITICAL_SECTIONS is set, the six lock types of ALib inherit this interface and thus can be optionally set to member DCSLock. If done, with the acquisition and release of the section, the state of the lock provided is asserted.

Three samples introduced by other ALib Modules quickly demonstrate this:

Sample 1:
Unique namespace instance GLOBAL_ALLOCATOR is accompanied by mutex GLOBAL_ALLOCATOR_LOCK. During bootstrap, this lock is attached to the allocator instance like this:
#if ALIB_DEBUG_CRITICAL_SECTIONS && ALIB_MONOMEM
#endif

Consequently, assertions are raised, if:
  • It is a debug-compilation,
  • Module ALib Threads is included in the distribution,
  • Compiler symbol ALIB_DEBUG_CRITICAL_SECTIONS is set, and
  • the GLOBAL_ALLOCATOR is used without prior acquisition of GLOBAL_ALLOCATOR_LOCK.
Sample 2:
Static member Formatter::Default is accompanied by mutex Formatter::DefaultLock. During bootstrap, this lock is attached to the formatter instance like this:
#if ALIB_DEBUG_CRITICAL_SECTIONS && ALIB_MONOMEM
Formatter::Default->DCSLock= &Formatter::DefaultLock;
#endif

The consequences are the same as described above: if all preconditions are met, the use of the default-formatter asserts that the corresponding mutex is acquired.
Sample 3:
Class TCondition is a next candidate for a potential lock usable with critical section testing. But because the type is a template and designed to be usable as a base type only, it technically can not implement the debugging interface AssociatedLock. But derived types can. This is, for example, the case with class ThreadPool. When critical section debugging is active, it attaches itself with field DCSLock of its MonoAllocator member. This ensures that allocations are only performed when the thread pool is locked.

Key Takeaways

  • Fundamental Purpose:
    The module primarily addresses two areas in multithreaded programming: launching/managing threads and protecting data against race conditions. It serves as a foundational component for multithreading support.
  • Role as a Code Selector:
    Beyond providing basic threading functionality, this module acts as a code selector for determining multithreading support across the entire ALib library.
  • Threading-Agnostic Design:
    ALib aims to support both, single-threaded and multithreaded environments, by making critical sections optional and pruning protection where it is unnecessary for performance optimization.
  • Cooperation Between Modules:
    The module’s design includes some integration with other ALib components to maintain flexibility and avoid unnecessary complexity, ensuring seamless multithreading support.
  • Debug-Build Assertions:
    Special debug assertions are implemented to mark critical sections and validate thread safety in debug-builds, promoting proactive identification of concurrency issues.
  • Historical Lesson:
    The design highlights the importance of context-aware synchronization, learning from early Java container implementations that misapplied method-level locking, resulting in performance penalties and misleading safety guarantees.
  • Practical Implementation:
    The module provides macros and types that work even in environments where threading support is excluded, maintaining flexibility and code simplicity.
  • Debug Support:
    Specific features like DbgCriticalSections assert proper acquisition and release of critical sections, allowing developers to catch issues during development, without using that are specialized for this purpose.

2. Thread Creation And Management

This module provides only basic support for thread creation, registration, and termination. The reason is that, this module (as elaborated in the introduction), has the role of switching the whole library to have thread support - or not.

More sophisticated thread management is found with the higher-level module ALib ThreadModel.

All that is available is

Their use is quite self-explanatory and documented with reference documentation of the types.

Note
It is no problem to have threads started using different methods and libraries than the one found here, as long as such threads become "native" operating system threads. If later such a thread uses method Thread::GetCurrent, a corresponding Thread object of ALib is created internally and returned. This way, the externally created thread is automatically "registered" with ALib.
If not stated differently in the detailed documentation, from this point in time, the thread can be interfaced with ALib classes in the same manner as if it was created using them. The same or similar should be true for the opposite situation.

3. Locks

In the area of data protection and controlling concurrent access to critical code sections, this module provides the following six types, which are wrappers around corresponding C++ Standard Library types:

ALib Type Wrapped C++ Standard Library Type
Lock std::mutex
TimedLock std::timed_mutex
RecursiveLock std::recursive_mutex
RecursiveTimedLock std::recursive_timed_mutex
SharedLock std::shared_mutex
SharedTimedLock std::shared_timed_mutex

The timed methods (of the timed lock versions), detect spurious wake-ups and mitigate those.

3.1 Performance

In respect to acquiring and releasing a previously unlocked instance, all types perform on modern GNU/Linux, MacOS, and Windows machines in the area of nanoseconds. Especially, no difference between the normal and the timed-versions seem to exist (when messured under GNU/Linux). Furthermore, the timed versions have the same footprint on these platforms.

Note
The designers of this module were hesitant to even separate the types. But we thought there might be a reason why the C++ Standard introduced the separation and so we followed this guideline. While a timed try to acquire a lock is a rather seldom requirement, when choosing the lock-type, a due reasoning about whether timed-tries will be required by a using code entities or not.

3.2 Debug Features

With debug-compilations, each lock receives a member called Dbg, which is of type DbgLockAsserter, respectively for the two shared-locks of type DbgSharedLockAsserter. This type stores caller information of recent acquisition and release actions. The information includes source code location, caller type-info, and the calling thread. Assertions produce exhaustive information, which tremendously helps to quickly identify many problems.

Note
The output should be "clickable" in your development environment. If it is not, the format can be changed so that it is recognized by a users' IDE. It is given with static fields DbgLockAsserter::ASSERTION_FORMAT for the non-shared lock types and with DbgSharedLockAsserter::ASSERTION_FORMAT_SHARED for the two shared lock types.

Assertions are raised, when

  • nested acquisitions on non-recursive locks are performed,
  • acquired locks are destructed,
  • a non-acquired lock is released, and
  • a lock is released, which is acquired by a different thread.

Furthermore, two sorts of warnings are given:

  • The three non-timed lock types are internally wrapping the corresponding timed versions of the C++ Library. With that, they optionally write debug-warning messages about waiting times higher than a given threshold. This threshold is found with debug-member DbgLockAsserter::WaitTimeLimit.
  • A maximum recursion threshold is provided with field DbgLockAsserter::WaitTimeLimit
  • Field DbgLockAsserter::RecursionLimit provides a threshold for the number of nested acquisitions, which defaults to 10. Note that recursion sometimes is not easily avoidable, but if a recursive lock is in fact needed, it should still not be overused. Of-course the threshold can be lifted if needed.
Note
Similar assertions are implemented with type TCondition.

3.3 Automatic Locking

3.3.1 Class Owner And Its Siblings

ALib provides a set of core classes which magically fit the interface of the locks found in this module. It is strongly advised to use their automation mechanics whenever possible.

Note
In fact this means using strong safety that C++ compilers provide in respect to unwinding the stack in case of emergency.

The main type of this set is class Owner which on construction invokes a method called Acquire with its templated type TOwnable and Release on destruction.

Along with the different Owner-types come three macros that makes their use easier by creating an "anonymous local variable". Those are:

Note
Anonymous local variables are not available by the C++ language. The macros do the trick, by internally using macro ALIB_IDENTIFIER.

The following table lists all Owner-types, the functions they call on their templated owned object, and the macro to use:

Owner Type Constructor Call Destructor Call Macro To Use
Owner Acquire Release ALIB_OWN
OwnerTry TryAcquire Release or ReleaseRecursive Not applicable
OwnerTimed TryAcquireTimed Release or ReleaseRecursive Not applicable
OwnerRecursive AcquireRecursive Release ALIB_OWN_RECURSIVE
OwnerShared AcquireShared Release ALIB_OWN_SHARED
OwnerTryShared TryAcquireShared Release Not applicable
OwnerSharedTimed TryAcquireSharedTimed Release Not applicable

The owners and their use are best explained with an example. Consider you have custom type MyAcquirable along with a global instance of this type:

struct MyAcquirable
{
#if ALIB_DEBUG
// the debug-version takes caller information
void Acquire(const alib::lang::CallerInfo& ci) { /*...*/ (void) ci; }
void Release() { /*...*/ }
#else
void Acquire() { /*...*/ }
void Release() { /*...*/ }
#endif
/*...*/
/*...*/
};
MyAcquirable myInstance;

Now, without using the macro, you would have to create a named owner instance for acquiring this instance:

// start a compound to determine the lifecycle of the owner
{
// create instance "myOwner", which otherwise never referenced and the variable
// name is superfluous.
// do stuff
// ... (this code never refers to "myOwner")
// ...
} // <- here myOwner is destructed and myInstance.Release() is called

Using macros makes things much simpler:

// start a compound to determine the lifecycle of the owner
{
// Using the macro, all is gone:
// - the variable name,
// - the templated custom Owner-type, and the
// - caller information that must only be given with debug-compilations.
ALIB_OWN(myInstance)
// do stuff
// ...
// ...
} // <- here the anonymous owner is destructed and myInstance.Release() is called

Those Owner-types that have the word "Try" in their name provide methods to receive the result of the acquisition, for example, OwnerTry::IsOwning.
Hence they must not be used anonymously, because the using code needs to check if a tried acquisition succeeded. For this reason, no corresponding macros are provided.

Note that the use of the macros from within static methods and namespace functions, needs one line of code as preparation. This is explained in chapter A.4 Collecting Caller Information of the General Programmer's Manual.

3.3.2 Lock-Macros

The different lock types introduced in previous section 3. Locks, are "compatible" with the Owner-types that ALib provides. Instead of the accompanying macros, aliased versions are recommended to be used:

Owner Type Original Macro Aliased Macros
Owner ALIB_OWN ALIB_LOCK
ALIB_LOCK_WITH
OwnerRecursive ALIB_OWN_RECURSIVE ALIB_LOCK_RECURSIVE
ALIB_LOCK_RECURSIVE_WITH
OwnerShared ALIB_OWN_SHARED ALIB_LOCK_SHARED
ALIB_LOCK_SHARED_WITH

The alias macros do not only increase readability, but they also prune their contents in case that module ALib Threads is not included in the ALib Distribution. Hence they support the creation of threading-agnostic software.

Note
As a background information:
Classes RecursiveLock and RecursiveTimedLock come with methods AcquireRecursive and ReleaseRecursive, instead of just Acquire and Release. This is a design decision. If this was not chosen, the standard Owner and corresponding macros could be used. However, the specific owner type OwnerRecursive was necessary to satisfy the different method names found with class DbgCriticalSections, where both acquirement functions are found in just one type. With that, the decision was taken to also use these function names also with the recursive
lock types. This way, the using code has a more explict language.

Note that the use of the macros from within static methods and namespace functions, needs one line of code as preparation. This is explained in chapter A.4 Collecting Caller Information of the General Programmer's Manual.

3.4. Specific Locks Found Across ALib

The following locks are found across this library. While their namespace and type names should hint to their use-case, for details, please consult their reference documentation.

4. Other Types Introduced

Two more fundamental types are introduced by this module. Those are:

Because this module is considered to be fundamental and low-level, other tools to manage threads and concurrency are not in its domain.
However, with module ALib ThreadModel, a high-level approach into threading is followed. It proposes an architecture for multithreaded software and provides classes that abstract away many plenty of the basics.