So in the meanwhile we know that double-checked-locking as is does not work in C++, at least not in a portable manner.
I just realised I have a fragile implementation in a lazy-quadtree that I use for a terrain ray tracer. So I tried to find a way to still use lazy initialization in a safe manner, as I wouldn't like to quadruple memory usage and re-order large parts of implemented algorithms.
This traversal is inspired by the pattern on page 12 of C++ and the Perils of Double-Checked Locking, but tries to do it cheaper:
(pseudo code!)
struct Foo {
bool childCreated[4];
Mutex mutex[4];
Foo child[4];
void traverse (...) {
...
if (!childCreated[c]) {
// get updated view
#pragma flush childCreated[c]
if (!childCreated[c]) {
ScopedLock sl (mutex[c]);
if (!childCreated[c]) {
create (c);
#pragma flush childCreated[c]
childCreated[c] = true;
}
}
}
}
}
It is assumed that #pragma flush
would also serve as a hard sequence point where compilers and processors won't be allowed to re-order operations across them.
Which problems do you see?
edit: Version 2, trying to take into account Vlads answer (introduce third flush):
(pseudo code!)
struct Foo {
bool childCreated[4];
Mutex mutex[4];
Foo child[4];
void traverse (...) {
...
if (!childCreated[c]) {
// get updated view
#pragma flush childCreated[c]
if (!childCreated[c]) {
ScopedLock sl (mutex[c]);
#pragma flush childCreated[c]
if (!childCreated[c]) {
create (c);
#pragma flush childCreated[c]
childCreated[c] = true;
}
}
}
}
}
edit: Version 3, I somehow find this pretty equivalent to Version 2, because I am not using the child itself but a primitive flag to check for validity, basically relying on a memory barrier between creating a child and writing to that flag.
(pseudo code!)
struct Foo {
bool childCreated[4];
Mutex mutex[4];
Foo child[4];
void traverse (...) {
...
if (!childCreated[c]) {
ScopedLock sl (mutex[c]);
#pragma flush childCreated[c]
if (!childCreated[c]) {
create (c);
#pragma flush childCreated[c]
childCreated[c] = true;
}
}
}
}