在 Rust 中编写双重检查锁定的正确方法是什么?
What is the right way to write double-checked locking in Rust?
我找到了 this article,但它看起来不对,因为 Cell
不能保证锁下的 set()
和锁上的 get()
之间的同步。
Atomic_.store(true, Ordering::Release)
是否影响其他非原子写操作?
我试着用看起来接近Java风格的AtomicPtr
来写,但失败了。我找不到在这种情况下正确使用 AtomicPtr
的示例。
Does Atomic_.store(true, Ordering::Release)
affect other non-atomic write operations?
是的。
实际上,Ordering
存在的主要原因是对非原子读写施加一些顺序保证:
- 在同一个执行线程中,对于编译器和CPU,
- 以便其他线程可以保证它们看到更改的顺序。
放松
约束越少Ordering
;唯一不能重新排序的操作是对相同原子值的操作:
atomic.set(4, Ordering::Relaxed);
other = 8;
println!("{}", atomic.get(Ordering::Relaxed));
保证打印4
。如果另一个线程读取 atomic
是 4
,它无法保证 other
是否是 8
。
Release/Acquire
写入和读取障碍分别为:
- 释放与
store
操作一起使用,并保证执行先前的写入,
- Acquire 将与
load
操作一起使用,并保证进一步的读取将看到至少与相应 [= 之前写入的值一样新鲜的值21=].
所以:
// thread 1
one = 1;
atomic.set(true, Ordering::Release);
two = 2;
// thread 2
while !atomic.get(Ordering::Acquire) {}
println!("{} {}", one, two);
保证 one
是 1
,并且对 two
只字不提。
请注意,具有 Acquire
负载的 Relaxed
存储或具有 Relaxed
负载的 Release
存储基本上没有意义。
请注意,Rust 提供 AcqRel
:它对存储的行为是 Release
,对于加载的行为是 Acquire
,因此您不必记住哪个是哪个……我知道但是不推荐它,因为提供的保证是如此不同。
SeqCst
最受限制Ordering
。保证一次跨所有线程排序。
What is the right way to write double-checked locking in Rust?
所以,双重检查锁定就是利用那些原子操作来避免不必要的锁定。
想法是有3件:
- 一个标志,最初为 false,一旦执行动作就为 true,
- 一个互斥锁,以保证初始化期间的排除,
- 一个值,待初始化。
并这样使用它们:
- 如果标志为真,值已经初始化,
- 否则,锁定互斥量,
- 如果标志仍然为假:初始化并将标志设置为真,
- 释放锁,值初始化完成
困难在于确保非原子 reads/writes 被正确排序(并以正确的顺序显示)。从理论上讲,您需要完整的围栏;在实践中,遵循 C11/C++11 内存模型的习语就足够了,因为编译器 必须 使其工作。
让我们先检查代码(简化):
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
where
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 = Some(f(value));
self.initialized.store(true, Ordering::Release); // (3)
}
}
unsafe { &*self.value.get() }.as_ref().unwrap()
}
}
有3个原子操作,通过注释编号。我们现在可以检查每种内存排序必须提供的正确性保证。
(1) 如果为真,则返回对值的引用,它必须引用有效的内存。这要求在原子变为真之前执行对该内存的写入,并且只有在它为真之后才执行对该内存的读取。因此 (1) 需要 Acquire
而 (3) 需要 Release
.
另一方面,(2) 没有这样的约束,因为锁定 Mutex
相当于一个完整的内存屏障:保证所有写入都发生在之前,所有读取只会发生在之后。因此,此负载不需要进一步保证,因此 Relaxed
是最优化的。
因此,就我而言,这种双重检查的实施在实践中看起来是正确的。
为了进一步阅读,我真的推荐 the article by Preshing,它在您链接的文章中有链接。它特别突出了理论(栅栏)和实践(原子 loads/stores 降低为栅栏)之间的差异。
我找到了 this article,但它看起来不对,因为 Cell
不能保证锁下的 set()
和锁上的 get()
之间的同步。
Atomic_.store(true, Ordering::Release)
是否影响其他非原子写操作?
我试着用看起来接近Java风格的AtomicPtr
来写,但失败了。我找不到在这种情况下正确使用 AtomicPtr
的示例。
Does
Atomic_.store(true, Ordering::Release)
affect other non-atomic write operations?
是的。
实际上,Ordering
存在的主要原因是对非原子读写施加一些顺序保证:
- 在同一个执行线程中,对于编译器和CPU,
- 以便其他线程可以保证它们看到更改的顺序。
放松
约束越少Ordering
;唯一不能重新排序的操作是对相同原子值的操作:
atomic.set(4, Ordering::Relaxed);
other = 8;
println!("{}", atomic.get(Ordering::Relaxed));
保证打印4
。如果另一个线程读取 atomic
是 4
,它无法保证 other
是否是 8
。
Release/Acquire
写入和读取障碍分别为:
- 释放与
store
操作一起使用,并保证执行先前的写入, - Acquire 将与
load
操作一起使用,并保证进一步的读取将看到至少与相应 [= 之前写入的值一样新鲜的值21=].
所以:
// thread 1
one = 1;
atomic.set(true, Ordering::Release);
two = 2;
// thread 2
while !atomic.get(Ordering::Acquire) {}
println!("{} {}", one, two);
保证 one
是 1
,并且对 two
只字不提。
请注意,具有 Acquire
负载的 Relaxed
存储或具有 Relaxed
负载的 Release
存储基本上没有意义。
请注意,Rust 提供 AcqRel
:它对存储的行为是 Release
,对于加载的行为是 Acquire
,因此您不必记住哪个是哪个……我知道但是不推荐它,因为提供的保证是如此不同。
SeqCst
最受限制Ordering
。保证一次跨所有线程排序。
What is the right way to write double-checked locking in Rust?
所以,双重检查锁定就是利用那些原子操作来避免不必要的锁定。
想法是有3件:
- 一个标志,最初为 false,一旦执行动作就为 true,
- 一个互斥锁,以保证初始化期间的排除,
- 一个值,待初始化。
并这样使用它们:
- 如果标志为真,值已经初始化,
- 否则,锁定互斥量,
- 如果标志仍然为假:初始化并将标志设置为真,
- 释放锁,值初始化完成
困难在于确保非原子 reads/writes 被正确排序(并以正确的顺序显示)。从理论上讲,您需要完整的围栏;在实践中,遵循 C11/C++11 内存模型的习语就足够了,因为编译器 必须 使其工作。
让我们先检查代码(简化):
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
where
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 = Some(f(value));
self.initialized.store(true, Ordering::Release); // (3)
}
}
unsafe { &*self.value.get() }.as_ref().unwrap()
}
}
有3个原子操作,通过注释编号。我们现在可以检查每种内存排序必须提供的正确性保证。
(1) 如果为真,则返回对值的引用,它必须引用有效的内存。这要求在原子变为真之前执行对该内存的写入,并且只有在它为真之后才执行对该内存的读取。因此 (1) 需要 Acquire
而 (3) 需要 Release
.
(2) 没有这样的约束,因为锁定 Mutex
相当于一个完整的内存屏障:保证所有写入都发生在之前,所有读取只会发生在之后。因此,此负载不需要进一步保证,因此 Relaxed
是最优化的。
因此,就我而言,这种双重检查的实施在实践中看起来是正确的。
为了进一步阅读,我真的推荐 the article by Preshing,它在您链接的文章中有链接。它特别突出了理论(栅栏)和实践(原子 loads/stores 降低为栅栏)之间的差异。