rust std::sync::Mutex::get_mut()不安全吗

qvtsj1bj  于 2023-10-20  发布在  其他
关注(0)|答案(3)|浏览(171)

塞雷赛
返回对基础数据的可变引用。由于这个调用可变地借用Mutex,所以不需要发生实际的锁定-可变借用静态地保证不存在锁。
所以问题是不需要锁定。当然,如果互斥体(in)没有直接从任何不安全的块中使用,那么任何线程都不可能持有对它的引用,因此不能使用它。这也意味着,为了编译get_mut的调用,如果任何线程正在使用它,则该线程必须已经完成其执行。任何新线程都可以访问受互斥锁保护的数据,因为它将从内存中读取,就像获取语义一样,因此通过get_mut对数据所做的任何更改对任何新线程都是可见的。
然而,如果一个互斥体是从一个不安全的块中使用的,那么有可能某个线程(像我的例子中的ffi)仍然在互斥体上有一个指针,并且可以持有锁并使用数据,或者只是完成数据的变异并解锁互斥体。在前一种情况下,它显然是UB,在后一种情况下,它是一个竞争条件,这也是UB,因为调用get_mut的线程可能看不到受保护数据的最新状态,该状态刚刚由另一个线程更新,该线程通过指针访问互斥体。为了使最新的更改在调用get_mut的线程上可见,应该保持获取/释放语义,但get_mut不调用任何获取操作。
get_mut是一个有用的函数,但我不相信它是线程安全的,因此应该是不安全的。

j1dl9f46

j1dl9f461#

Rust中可变引用的主要属性是它们是unique。也就是说,当代码作为一个整体是合理的,它保证了 * 唯一 * 的方式来访问可变引用引用的值是通过确切的引用。在安全代码中,这是自动执行的。在不安全的代码中,包括FFI,这是不变量 you,作为编写unsafe的人,应该维护。
特别是,当你有对Mutex的可变引用时,让任何其他地方访问同一个互斥体或它后面的数据都是显式和立即的UB。特别是,让其他人锁定它本质上是UB,因为根据定义,解锁将访问互斥体(以存储“未锁定”位)。
因此,如果某些unsafe代码导致get_mut是非线程安全的,那么这种不安全的代码本身就是不可靠的。

wd2eg0qa

wd2eg0qa2#

如果您只使用安全代码,那么可变引用的存在就意味着您在编译时有一个happens-before关系,因此是同步的。
如果你使用不安全的代码来访问锁,那么你有责任确保存在happens-before关系。如果不这样做,即使没有实际更改数据,代码也是不可靠的,因为您正在创建两个重叠的可变引用,而没有同步,因此是UB。因此,您负责创建正确的同步。如果你做了,一切都很好。如果你不这样做,* 你的 * 不安全代码是不健全的,UB来自 * 你的 * 不安全代码。std的get_mut()在这里不起作用,因此是合理的。

mqxuamgl

mqxuamgl3#

我的答案是,是的,当然它应该是不安全的。在ARM上运行这个程序(x64可以很好地工作,因为内存很强)。有两个不安全的块,它们的所有安全条件都得到满足,因此这是一个合理的(从借用检查器的Angular 来看)代码,但它不是线程安全的。将两个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) {
        // Because `fetch_sub` is already atomic, we do not need to synchronize
        // with other threads unless we are going to delete the object. This
        // same logic applies to the below `fetch_sub` to the `weak` count.
        if self.inner().strong.fetch_sub(1, Release) != 1 {
            return;
        }

        // This fence is needed to prevent reordering of use of the data and
        // deletion of the data. Because it is marked `Release`, the decreasing
        // of the reference count synchronizes with this `Acquire` fence. This
        // means that use of the data happens before decreasing the reference
        // count, which happens before this fence, which happens before the
        // deletion of the data.
        //
        // As explained in the [Boost documentation][1],
        //
        // > It is important to enforce any possible access to the object in one
        // > thread (through an existing reference) to *happen before* deleting
        // > the object in a different thread. This is achieved by a "release"
        // > operation after dropping a reference (any access to the object
        // > through this reference must obviously happened before), and an
        // > "acquire" operation before deleting the object.
        //
        // In particular, while the contents of an Arc are usually immutable, it's
        // possible to have interior writes to something like a Mutex<T>. Since a
        // Mutex is not acquired when it is deleted, we can't rely on its
        // synchronization logic to make writes in thread A visible to a destructor
        // running in thread B.
        //
        // Also note that the Acquire fence here could probably be replaced with an
        // Acquire load, which could improve performance in highly-contended
        // situations. See [2].
        //
        // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html)
        // [2]: (https://github.com/rust-lang/rust/pull/41714)
        acquire!(self.inner().strong);

        unsafe {
            self.drop_slow();
        }
    }

所以现在,我对std::sync::Mutex的整个不合理性有了更强烈的看法。只有互斥锁的锁族方法是可以的,get_mutdrop不是线程安全的。记住,Mutex抽象的全部意义在于通过公共安全API提供线程安全访问->确保任何公共安全方法在任何安全场景中彼此之间都有happens-before关系。
回到Arc::drop,它已经有一个划痕来解决Mutex::drop中的bug。如果Mutex::drop能确保与所有其他Mutex公共安全API函数之间的happens-before关系,那么Arc可以对每种类型使用简单的宽松排序。
1.假设Arc<T>中的T没有内部可变性,那么rust语言保证在计数器上使用宽松顺序总是安全的,因为对受保护数据的唯一访问是加载。
1.让我们假设Arc<T>中的T暴露了内部可变性,那么Arc应该确保happens-before关系(c++ std::shared_ptr所做的并被rust模仿)或者(这更符合逻辑)具有内部可变性的类型T必须确保其公共安全API中的happens-before关系。
现在,由于ArcMutex设计选择之间错误的责任分配,所有其他类似于Arc的代码都必须模仿这种糟糕的设计,并确保happens-before关系。
另一个参数,在一个普通的程序中,Arc::drop调用与Mutex::drop调用的比例是多少?我打赌比1少得多。因此,在Mutex中确保happens-before关系比在其他任何地方都更理想,这是Mutex抽象在像rust这样的安全语言中应该做的事情。
同样,拥有&mut ref并不假设在已删除的数据上有任何happens-before关系,它只保证没有任何其他ref,在我的例子中,情况就是这样。还记得Mutex的内部结构吗?它包含UnsafeCellsys::Mutex,在我的例子中,sys::Mutex上的任何操作都是线程安全的,并且它由sys::Mutex实现保证,但访问UnsafeCell不是,需要happens-before关系,Mutex是什么?
换句话说,如果你将Mutex抽象分解为一对sys::MutexUnsafeCell,并在这个装箱的对上传递一个指针,那么在将指针转换回引用之后,你可以在所有场景中安全地使用sys::Mutex部分,但是你不能在不确保happens-before关系的情况下使用UnsafeCell
PPS:基本上,所有的论点,从那些否决这个答案的人,可以缩小到一个简单的句子:
在rust中,在任何安全代码中使用std::sync::atomic::Ordering::Relaxedstd::sync::AtomicPtr都是非法的。
我可以离开并接受这个观点,当这将被编码在生 rust 类型系统。

相关问题