ace condition happens when a
thread-shared resource is not properly protected. The protection itself is an
expensive operation. Hence, the programmer tends to optimize and makes errors.
Such errors are hard to detect because of the nature of the race condition. We
follow ideas of the article
[Alexandrescu1]
and
provide tools for compiler-assisted prevention of such errors. We define Guard
objects and follow the following design rules:
Definition
(Multi-threaded design rules)
1. Thread-safe member functions are declared volatile.
2. Thread-unsafe member functions are not volatile.
3. Locked shared objects are passed into functions as Guard objects.
4. Unlocked shared objects are passed into functions with volatile type
modifier.
Consider the following code:
#include <ots/threading/RWMutex.hpp>
#include <ots/threading/WriterGuard.hpp>
#include <ots/threading/ReaderGuard.hpp>
#include <ots/otsConfig.hpp>
class X
{
public:
typedef
ots::config::RWMutex::type Mutex;
private:
mutable
Mutex theMutex;
std::string
theString;
public:
Mutex&
mutex() const { return theMutex; }
void
set( const char* x ) { theString=x; }
std::string
get() const { return theString; }
};
void func1(volatile X& x)
{
typedef
ots::config::WriterGuard<X,X::Mutex>::type Guard;
Guard(x)->set("Hello.");
}
void func2(volatile X& x)
{
typedef
ots::config::ReaderGuard<X,X::Mutex>::type Guard;
std::string
s=Guard(x)->get();
}
The class X is meant for multi-threaded access. The interaction with the Guard
primitives requires that the class X would expose its mutex via the member
function mutex().
The func2 reads contents of some class X. It must use the ReaderGuard that
removes volatileness and exposes only the const-part of the interface of the
class X. An attempt to access the x directly results in a compiler error.
The func1 writes into the class X and uses the WriterGuard to access the
entire interface.
For all access to the threading primitives we use the metafunctions defined in
the otsConfig.hpp. The file otsConfig.hpp is declarative: we still need to
include the concrete implementation headers RWMutex.hpp, WriterGuard.hpp and
ReaderGuard.hpp.
Such organization does not prevent from unlocked modification of primitive
data types using built-in operations. The following code compiles.
class X
{
private:
double
theX;
public:
void
f() volatile
{
theX++;
}
};
This problem may be solved with a pointer indirection but this would also
create to much of performance hit. We also could have preprocessor-based DEBUG
and RELEASE versions but the difference between the versions would defeat the
purpose. In the present project this problem is left unsolved.
|