3 ms·
To spell it out a bit more for other readers, this article is doing effectively this: get() { if needsinit_or_wait_if_initting() { // lock (atomics)
by Groxx 3y ago
To spell it out a bit more for other readers, this article is doing effectively this:
get() {
if needsinit_or_wait_if_initting() { // lock (atomics) then check
thing = init_thing()
}
return thing
}
Which is fine because the check always occurs before reads or writes.
The classic double-checked lock mistake looks something like this, when following this example:
get() {
// "fast" path: get thing if it exists, because
// the code looks like that means it's done, right?
// no check because checks are slow and we move fast!
if thing != nil { // first check
return thing
}
// slow path: use slow stuff which is slow :(
if needsinit_or_wait_if_initting() { // lock then second check
thing = init_thing()
}
return thing
}
It's that un-guarded see-if-nil-and-return that makes it unsafe, because the assignment is not guaranteed to be "complete" in many cases. There is no explicit relationship between the initialization, assignment, and read, so the compiler can do stuff that doesn't match what the code looks like, e.g. reorder "save the new memory address to `thing`" before "init the thing" (so you temporarily have an uninitialized non-nil object), and at runtime you can get stuff like "only the first half of the memory synced to that CPU core so you got a busted object".
- gpderetta 3y agoThe double checked locking is perfectly fine as long as the fast path 'thing' access has the right semantics (in C and C++ you need an atomic load with consume ordering).