The fundamental two topics that a class library dedicated "multithreaded programming" adress are:
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.
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.
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:
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.
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:
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.
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.
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
If executed in parallel threads, these three steps comprise a critical section. Now, if the value resides in type ContainerType, formally the instructions are:
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.
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:
With this theory discussed, type DbgCriticalSections can now be quickly explained.
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.
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:
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.
true
for the opposite situation.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.
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.
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.
Assertions are raised, when
Furthermore, two sorts of warnings are given:
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. 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.
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:
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:
Now, without using the macro, you would have to create a named owner instance for acquiring this instance:
Using macros makes things much simpler:
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.
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 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.
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.
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.