Threads in the Gabi Software Library
Status
Threading is currently very experimental.
There are no tests for thread-safety. To be frank, I'm not sure how
to develop such tests.
Preconditions
The thread safety here presupposes that static initialization runs in a
thread safe context, and that memory will be fully synchronized after
static initialization. If initialization occurs before main is called,
which is the case for statically linked modules in all compilers I am
aware of, and no threads are created in static initializers, the
conditions are met.
I'm less sure about what happens when dynamically linking is involved.
Presumably, the runtime linker will run the initialization routines in
its own thread, isolated from the rest of the program. Which still
leaves two questions to be answered:
- what happens if the initialization routines call routines in other
parts of the program, which aren't in the dynamic object, and
- what guarantees do we have that the memory initialized in the
initialization routines is fully synchronized with all of the
previously existing threads?
Singletons
Our thread safe singleton is implemented as follows:
- Components:
-
The singleton contains three critical components, the static
instance function, a static pointer to the single instance, and a
static createInstance function.
- Initialization:
-
The static pointer is initialized by the createInstance function,
e.g.
MySingleton* MySingleton::ourInstance = MySingleton::createInstance() ;
The goal here is to ensure that createInstance is called before
static initialization has finished, and thus, before threading has
begun. A critical invariant here is that this variable must not
change its value once threading has been started.
- createInstance
-
This function is not thread-safe, and should only
be called before threading is active. It checks that the pointer is
null, returning either the value of the pointer, or a new instance,
e.g.:
MySingleton*
MySingleton::createInstance()
{
return ourInstance == NULL
? new MySingleton()
: ourInstance ;
}
- instance
-
This is the only function visible at the user interface level. It
checks if ourInstance is NULL, and calls createInstance if so, e.g.
MySingleton&
MySingleton::instance()
{
if ( ourInstance == NULL ) {
ourInstance = createInstance() ;
}
return *ourInstance ;
}
Two situations must be considered: when the function is called
during static initialization, and later. In the first case, because
of order of initialization issues, there is no guarantee as to the
value of ourInstance. It could be null, and createInstance will be
called. However, since threading has not yet started, there should
be no problem. In the second case, because static initialization
has finished, we know that createInstance has already been called at
least once, and that the value of ourInstance is immutable and not
null. Since the value is immutable, no synchronization is needed to
ensure correct access to the variable, and since the value is not
null, the non-thread-safe function createInstance will not be
called.
Note too that this depends on the correct execution of zero
initialization, before any dynamic initialization runs, so that
createInstance and instance are guaranteed to see a null pointer in
ourInstance if the pointer hasn't been (dynamically) initialized.
Dynamic objects
I'm not too sure about this with dynamically linked objects (shared
objects in Posix, dynamicall linked libraries in Windows). Hopefully,
all static initialization will take place in a context totally isolated
from any threading that might already be active, so the code itself
should work. The second question is synchronization. Are all threads
(including those started before the dynamic object was loaded)
guaranteed to see the correctly initialized values? A priori, I find it
difficult to imagine that this is not the case, but Posix doesn't list
dlopen in the list of functions which guarantee memory synchronization.