我的问题的答案是,是的,当然是不安全的。在ARM上运行(x64因为内存强大所以运行良好)。有确切的两个不安全块,并且它们的所有安全条件都得到满足,因此从借用检查器的角度来看,这是一个合理的代码,但它不是线程安全的。将两个get_mut替换为锁定操作可以解决问题,正如预期的那样。
use std::sync::atomic::AtomicPtr;
use std::sync::atomic::Ordering::*;
use std::sync::Mutex;
static RX: AtomicPtr<Mutex<i32>> = AtomicPtr::new(std::ptr::null_mut());
static TX: AtomicPtr<Mutex<i32>> = AtomicPtr::new(std::ptr::null_mut());
const COUNT: i32 = 64 * 1024 * 1024;
fn main() {
let t = std::thread::spawn(|| {
let mut current = std::ptr::null_mut();
let mut m;
for i in 0..COUNT {
loop {
match RX.compare_exchange(current, std::ptr::null_mut(), Relaxed, Relaxed) {
Ok(ptr) if !ptr.is_null() => {
m = unsafe { Box::from_raw(ptr) };
break;
}
Ok(ptr) | Err(ptr) => current = ptr,
}
}
assert_eq!(m.get_mut().unwrap(), &-i);
*m.get_mut().unwrap() = i;
TX.store(Box::<_>::into_raw(m), Relaxed);
}
});
let mut m = Box::new(Mutex::new(0));
for i in 0..COUNT {
*m.get_mut().unwrap() = -i;
RX.store(Box::<_>::into_raw(m), Relaxed);
let mut current = std::ptr::null_mut();
loop {
match TX.compare_exchange(current, std::ptr::null_mut(), Relaxed, Relaxed) {
Ok(ptr) if !ptr.is_null() => {
m = unsafe { Box::from_raw(ptr) };
break;
}
Ok(ptr) | Err(ptr) => current = ptr,
}
}
assert_eq!(m.get_mut().unwrap(), &i);
}
t.join().unwrap();
}
PS:我决定在这里加上额外的解释,因为评论对这个并不起作用。让我们来看一下
Arc::drop
和它们的评论。
fn drop(&mut self) {
if self.inner().strong.fetch_sub(1, Release) != 1 {
return;
}
acquire!(self.inner().strong);
unsafe {
self.drop_slow();
}
}
现在,我对整个不健全的std::sync::Mutex的观点更加坚定。只有mutex的lock方法族是可以的,get_mut和drop不是线程安全的。请记住,Mutex抽象的整个目的是通过公共安全API提供线程安全访问,确保任何公共安全方法在任何安全场景中彼此具有happens-before关系。
回到Arc::drop,它已经在解决Mutex::drop中的错误。如果Mutex::drop能够确保与所有其他Mutex的公共安全API函数具有happens-before关系,那么Arc可以对每种类型使用简单的松散排序。
假设在`Arc`中,`T`没有内部可变性,那么根据Rust语言的保证,对计数器使用松散顺序总是安全的,因为对受保护数据的唯一访问是加载操作。
假设在`Arc`中,`T`暴露了内部可变性,那么要么`Arc`应该确保happens-before关系(就像C++的`std::shared_ptr`一样,并且Rust模仿了这种行为),要么(更合乎逻辑的是)具有内部可变性的类型`T`必须在其公共安全API中确保happens-before关系。
现在,由于`Arc`和`Mutex`设计选择上的错误责任分配,所有类似于`Arc`的其他代码都必须模仿这种糟糕的设计并确保happens-before关系。
另一个论点是,在普通程序中,
Arc::drop
调用与
Mutex::drop
调用的比例是多少?我敢打赌远远小于1。因此,确保发生在
Mutex
中的 happens-before 关系比在其他地方更加优化,归根结底,这就是像 Rust 这样的安全语言中
Mutex
抽象应该做的事情。
再次强调,拥有 &mut 引用并不意味着对受保护数据有任何 happens-before 关系,它只保证没有其他引用存在,而在我的例子中,正是如此。请记住,
Mutex
的内部结构包含
UnsafeCell
和
sys::Mutex
,在我提供的例子中,对
sys::Mutex
的任何操作都是线程安全的,这是由
sys::Mutex
的实现保证的,但对
UnsafeCell
的访问则需要 happens-before 关系,这就是
Mutex
的用途,不是吗?
换句话说,如果你将
Mutex
抽象拆解为一对
sys::Mutex
和
UnsafeCell
,并将指针传递给这个封装的对组,然后将指针转换回引用,你可以在所有情况下安全地使用
sys::Mutex
部分,但是如果没有确保先行发生关系,你不能这样做
UnsafeCell
。
附言:基本上,所有反对这个答案的人的论点都可以归结为一个简单的句子:
在Rust中,在任何安全代码中使用
std::sync::AtomicPtr
和
std::sync::atomic::Ordering::Relaxed
是非法的。
当这一点被编码到Rust类型系统中时,我可以接受并认同这个观点。
get_mut
需要对Mutex
的可变引用。如果你持有对Mutex
的可变引用,那么其他人就不会持有对它的任何引用,无论如何。由于没有其他引用,就不会出现竞争条件。因此,不需要进行锁定。 - undefinedget_mut()
的访问之间没有_acquire_,对吗? - undefined"Rust借用检查器对线程和竞态条件一无所知"
-> 是的,它知道的,可以查看Sync
和Send
特性。 - undefined