concurrency - What is the right way to write double-checked locking in Rust? -
i found this article, looks wrong because cell
not guarantee synchronization between set()
under lock , get()
on lock.
does atomic_.store(true, ordering::release)
affect other non-atomic write operations?
i tried write atomicptr
looks close java-style failed. couldn't find examples of correct using of atomicptr
in such cases.
does
atomic_.store(true, ordering::release)
affect other non-atomic write operations?
yes.
actually, primary reason ordering
exists impose ordering guarantees on non-atomic reads , writes:
- within same thread of execution, both compiler , cpu,
- so other threads have guarantees in order in see changes.
relaxed
the less constraining ordering
; operations cannot reordered operations on same atomic value:
atomic.set(4, ordering::relaxed); other = 8; println!("{}", atomic.get(ordering::relaxed));
is guaranteed print 4
. if thread reads atomic
4
, has no guarantee whether other
8
or not.
release/acquire
write , read barriers, respectively:
- release used
store
operations, , guarantees prior writes executed, - acquire used
load
operations, , guarantees further reads see values @ least fresh ones written prior correspondingstore
.
so:
// thread 1 one = 1; atomic.set(true, ordering::release); 2 = 2; // thread 2 while !atomic.get(ordering::acquire) {} println!("{} {}", one, two);
guarantees one
1
, , says nothing two
.
note relaxed
store acquire
load or release
store relaxed
load meaningless.
note rust provides acqrel
: behaves release
stores , acquire
loads, don't have remember which... not recommend though, since guarantees provided different.
seqcst
the constraining ordering
. guarantees ordering across threads @ once.
what right way write double-checked locking in rust?
so, double-checked locking taking advantage of atomic operations avoid locking when unnecessary.
the idea have 3 pieces:
- a flag, false, , true once action has been executed,
- a mutex, guarantee exclusion during initialization,
- a value, initialized.
and use them such:
- if flag true, value initialized,
- otherwise, lock mutex,
- if flag still false: initialize , set flag true,
- release lock, value initialized.
the difficulty ensuring non-atomic reads/writes correctly ordered (and become visible in correct order). in theory, need full fences that; in practice following idioms of c11/c++11 memory models sufficient since compilers must make work.
let's examine code first (simplified):
struct lazy<t> { initialized: atomicbool, lock: mutex<()>, value: unsafecell<option<t>>, } impl<t> lazy<t> { pub fn get_or_create<'a, f>(&'a self, f: f) -> &'a t f: fnonce() -> t { if !self.initialized.load(ordering::acquire) { // (1) let _lock = self.lock.lock().unwrap(); if !self.initialized.load(ordering::relaxed) { // (2) let value = unsafe { &mut *self.value.get() }; *value = f(value); self.initialized.store(true, ordering::release); // (3) } } *self.value.get() } }
there 3 atomic operations, numbered via comments. can check kind of guarantee on memory ordering each must provide correctness.
(1) if true, reference value returned, must reference valid memory. requires writes memory executed before atomic turns true, , reads of memory executed after true. (1) requires acquire
, (3) requires release
.
(2) on other hand has no such constraint because locking mutex
equivalent full memory barrier: writes guaranteed have occured before , reads occur after. such, there no further guarantee needed load, relaxed
optimized.
thus, far concerned, implementation of double-checking looks correct in practice.
for further reading, recommend the article preshing linked in piece linked. notably highlights difference between theory (fences) , practice (atomic loads/stores lowered fences).
Comments
Post a Comment