C++ Multithreading: Library Primitives

Kevlin Henney kevlin at curbralan.com
Thu Sep 9 16:49:53 BST 2004


Hi all,

OK, here's some drafted thoughts concerning library primitives. I have 
not yet included any references.

Kevlin

<blurb>
Library Primitives to Support Multithreaded Programming
=======================================================
Against a standardized, clearly defined, and sufficiently portable 
memory model that addresses threading issues explicitly, a C++ 
programmer expecting to work with threads would likely expect standard 
library support for thread programming primitives. There are many forms 
and styles such a library can take, and many examples of such libraries 
in production code. The goal of the current proposal is not to define 
such a library in detail, but to outline requirements and reasonable 
expectations of such a library.

Level of Library
----------------
High-level threaded programming models and utilities are ultimately 
desirable, but must be built on a more primitive and portable layer. A 
standard C++ threading library should aim to be that primitive and 
portable layer rather than a C library, no matter how standard or well 
known a given C API is. It is most likely that any threading library 
implementation would build on an existing C threading API, but that is a 
matter for the library implementation rather than the user of the 
library.

This proposal is therefore conservative in focusing on library 
primitives rather than higher-level facilities. It would be reasonable 
to also standardize such facilities, but they are considered to be a 
separate and additional consideration, layering on top of the library 
primitives. Until progress is made on the memory model and the library 
primitives, extending the proposal with higher-level facilities might be 
considered premature.

Portability of Library
----------------------
A platform may or may not support preemptive a multithreading model 
natively. Non-preemptive threading models can be made to look and behave 
superficially like their preemptive counterparts in certain simple 
cases, but they are sufficiently different to work with that this 
proposal focuses on what is these days presumed to be the default 
threading approach in programmers' minds, namely preemptive 
multithreading. As such, threading support in the library is not 
required to extend to non-preemptive models. Conversely, it may not be 
possible to use a standard threading library on all platforms that 
support other C++ standard library features.

Therefore, a question that needs to be resolved in proposing a threading 
library is to define its kind of conformance. A _freestanding_ C++ 
implementation, by definition, need not include threading facilities in 
its library since its support for the standard library is minimal. 
However, it might be considered too much of a burden on what a _hosted_ 
implementation must provide if, to conform to the standard, thread 
facilities must be supported. There are a number of possible approaches 
to consider:

(1) Add a new kind of implementation, along the lines of _freestanding_, 
_hosted_, and _hosted with threads_ (or some suitable synonym). The 
_hosted_ category can be considered to be the library as it is defined 
in the current standard (along with any other nonthread-related 
extensions proposed for the next standard), whereas the _hosted with 
threads_ category would include the whole library.

(2) Provide the threading primitives library as an optional part of the 
library, perhaps as an appendix. Although this might conceptually be 
similar to approach (1), its spirit and practicalities are subtly 
different. Optionality implies feature testing, and so some feature 
testing mechanism is needed, such as macro testing (as used in POSIX), 
tag types, or traits. However, the inclusion of one optional library 
might set a precedent and be considered a cue for other optional 
libraries.

(3) Require the definition of threading primitives within the standard 
library, but leave its viability as a QoI matter: code would compile but 
would not necessarily run successfully, although its execution would not 
be undefined. For example, an implementation on a single-threaded 
platform might chose to throw an exception for any attempted thread 
creation, or synchronization primitives might be implemented as 
stateless objects with null implementations of their locking functions. 
This approach could be complemented by feature-checking mechanisms.

In addition to whether or not threading is supported fully in a given 
library implementation, there is also per-platform variation in the 
features supported. Because C++ is still considered a systems 
programming language, there is an expectation of a close (but no closer 
than necessary) correspondence between its primitives and the primitives 
of the platform. This means that any offering of library primitives must 
strike a balance between being a pure and common subset of what is 
common across platforms -- but perhaps too small a subset to be useful 
in real-world applications -- and a constructed superset of what is 
available -- demanding more of a library implementor. The superset 
approach may take an implementation too far from the correspondence 
between the platform and the library primitives, making constructs that 
are efficient on one platform indistinguishable from those that are 
inefficient, because the former can be realized directly and the latter 
must be constructed and might require elaborate support.

For example, although mutexes are commonly supported, they tend to 
appear in different flavors (e.g. reentrant and non-reentrant). 
Platforms vary in their support for which flavors are supported -- some 
support just one, some support many. Most users would likely expect that 
these were close to their underlying platform primitives. Types could be 
resolved at compile time or at runtime.

Another case that may warrant a quality of implementation license is 
that of deadlock detection. Although convenient, it is not universally 
supported and not necessarily efficient for all programmer's needs, 
especially when they have the choice and confidence of not using it. A 
reasonable quality of implementation constraint would be that, in the 
event of deadlock, an implementation may either block indefinitely (or 
until a timeout, if a lock has one specified) or throw an exception to 
indicate a deadlock condition.

In other cases there is a difference in the platform primitives on 
offer. For example, Win32 offers event variables but not conditional 
variables, and pthreads offers condition variables but not event 
variables. Assuming a mutex primitive, it is possible to implement one 
in terms of the other without too much infrastructure or surprise. 
However, in this particular case, condition variables are generally seen 
as the superior alternative and event variables as too primitive, so it 
would be reasonable for a standard library to support condition 
variables but not event variables.

Similarly, synchronization primitives vary in their scope, i.e. 
available only within a process or also visible to other processes. 
Win32 treats its _mutex_ primitive as having interprocess visibility and 
its _critical section_ primitive, which has mutex semantics, as being 
process local only. In this case, because standard C++ does not have a 
concept of separate processes, it is sufficient to focus on 
single-process scope for standardization, but acknowledge that a library 
implementor may reasonably choose to extend the library to accommodate 
interprocess communication.

However, no matter what set of primitives is considered sufficiently 
portable, it must be recognized that underlying threading platforms are 
invariably richer, often providing specific features that a programmer 
may wish to take advantage of, e.g. interprocess synchronization and 
real-time scheduling. Therefore, it is probable rather than just 
possible that specific implementations will extend a core standardized 
threading library. Any chosen library design must take this into account 
and be open to these kinds of extensions.

Library Features
----------------
There are broadly two areas in which a library needs to offer 
primitives:

(1) Thread execution: The launching, termination, and joining of 
threads. Whether and to what degree library primitives are offered to 
support manipulation of thread execution priorities, specification of 
scheduling model (e.g. FIFO or round-robin), and thread cancellation is 
currently open for discussion, and depends largely on whether a 
standardized library adopts a pure common subset approach, a common 
subset with optional extensions, or a required superset approach.

(2) Synchronization: This is the traditional domain of locks, but also 
includes atomic operations for lock-free programming. There is some 
range of variation in what primitives are supported (e.g. binary 
semaphores, counting semaphores, mutexes, condition variables, event 
variables) and in the way that they are supported (e.g. mutex 
reentrancy). A library needs to offer a reasonable minimum set of 
synchronization primitives and a way of dealing with the feature 
variability within each type.

In principle, if a higher-level set of facilities were also offered as 
part of a standard threading library, they should be fully and portably 
implementable in terms of the library primitives. This is not to say 
that they would be required to be implemented in terms of them, but that 
they could be (cf the relationship between I/O streams and the C 
standard I/O facilities).

Style of Library
----------------
Although a threading and synchronization library is intended to be 
primitive, that does not mean it has to be at the lowest level from a 
C++ programmer's perspective: an existing C library would otherwise be 
sufficient. A user of a standard C++ threading library would reasonably 
expect such a library to make best use of the language features and 
programming idioms available, which favors an approach based on objects 
rather than on function pointers and void pointers, etc.

There are two basic approaches to defining active objects: one is to 
inherit threadedness from a base class and the other is to use a 
threading object to execute a function on a separate object in a thread. 
The former was traditionally popular, but the delegation-based approach 
is now considered both a better design, in terms of its separation of 
concerns, and the more popular style, both in existing C++ threading 
libraries and other languages. In C++ the most idiomatic realization of 
the delegation-based approach is to execute function objects, as opposed 
to requiring that a threadable object inherit from a library-specified 
base class with a single virtual ordinary function member that needs to 
be overridden. The function object approach is based on concrete types 
and templates, supporting uniformity of use for both function objects 
and function pointers.

More generally, the generic approach suggests itself as the approach to 
be used for defining library primitives for threading and 
synchronization. This approach has not been taken in any of the popular 
C++ threading libraries, but it has been the subject of some work by one 
of the authors of this proposal. Following the existing example of the 
STL, a generic approach to threading divides a primitives library into 
two aspects: a set of requirements on types (concepts) and a 
standardized set of types for out-of-the-box use (cf Sequence 
requirements and the std::deque, std::list, and std::vector class 
templates). This approach offers a ready-to-use and standard code 
library, but also offers an open model for extension for both users and 
library implementors.

For the threading side of the library, requirements would be needed to 
define at least what functions and function objects could be threaded, 
the way in which they were launched, and the mechanism by which they 
could be joined, and any results recovered. One model that has been 
suggested is that thread launching can be treated as an application of a 
function or function object (a threader) to the function to be run in a 
thread. Treating a threader as a function object allows libraries to 
extend behavior by overloading the constructor to handle specification 
of potentially platform-specific features, such as stack size, 
scheduling policy, etc, without disrupting the uniformity of the 
function call syntax. Alternatively, different threader types could be 
provided that satisfy the threader requirements but realized specific 
thread-execution policies, e.g. thread pooling. A consideration is to 
allow threadable functions be to return a value that the user can pick 
up through an explicit join action. C APIs commonly accommodate this 
feature via a void * or equivalent. Object-wrapped threading libraries 
normally ignore this return bandwidth, so that a join operation returns 
void. A standard C++ threading library could handle type-safe value 
returns simply and generically.

The variety of operations that a locked synchronization primitive can 
support (e.g. blocking lock, nonblocking lock, lock with timeout) is 
large enough and diverse enough that mandating their full support on all 
locking primitives might be considered too much of an overhead by both 
users and implementors alike. A hierarchical set of categories defining 
lockability, similar to the iterator categories in the current standard 
library, might offer a reasonable approach to addressing this 
variability. A library could define traits and tag types to allow 
programmers discover details of a given primitive or to choose the best 
fit for their needs.

On their own, objects satisfying some category of lockability 
requirements, whether primitives defined in the library or higher-level 
objects written by the user, could be tedious and error prone to use. 
Inevitably their use would be wrapped up, using the _scoped locking_ 
idiom, a specific and common application of the _resource acquisition is 
initialization_ idiom). The approach is common enough that provision of 
some scoped-locking-based helpers would most likely be expected by users 
of a standard threading library. The set of such lockers is potentially 
unbounded, and is not restricted to the common scope lock: for example, 
smart pointers that wrap individual function calls can be defined in 
terms of a stable set of lockability requirements. In this sense, 
lockers are to lockable objects as algorithms are to iterators. A 
library could provide some common helper types, but the formalizing of 
lockability requirements would allow library implementors and users 
alike a uniform model for extension that would be nonintrusive on 
existing lockable objects, whether standardized synchronization 
primitives or other user-defined lockable objects.
</blurb>
-- 
____________________________________________________________

   Kevlin Henney                   phone:  +44 117 942 2990
   mailto:kevlin at curbralan.com     mobile: +44 7801 073 508
   http://www.curbralan.com        fax:    +44 870 052 2289
   Curbralan: Consultancy + Training + Development + Review
____________________________________________________________






More information about the cpp-threads mailing list