std::sync::Mutex::get_mut() 应该不安全吗?

5

文档说

返回对底层数据的可变引用。 由于此调用可变地借用了 Mutex,因此不需要进行实际的锁定 - 可变借用在静态上保证不存在任何锁。

所以问题是不需要锁定。当然,如果互斥锁(间接地)没有从任何不安全的块中使用,则不可能有任何线程持有对它的引用,因此也不能使用它。这也意味着,在编译 get_mut 的调用时,如果有任何线程正在使用它,那么该线程必须已经完成执行。任何新线程访问由互斥锁保护的数据也是可以的,因为它将从内存中读取,就像获取语义一样,因此通过 get_mut 对数据进行的任何更改将对任何新线程可见。

然而,如果在不安全的代码块中使用了互斥锁,那么可能会出现某个线程(可能是 ffi,就像在我的情况下一样)仍然持有互斥锁的指针,并且可以同时持有锁并使用数据,或者完成对数据的修改并解锁互斥锁。在前一种情况下,这显然是未定义行为;在后一种情况下,这是一种竞争条件,同样也是未定义行为,因为调用 get_mut 的线程可能无法看到由另一个通过指针访问互斥锁的线程刚刚更新的受保护数据的最新状态。为了使最新的更改在调用 get_mut 的线程上可见,应该使用获取/释放语义,但是 get_mut 没有调用任何获取操作。
get_mut 是一个有用的函数,但我不认为它是线程安全的,因此应该标记为不安全。

3
回想一下类型系统!get_mut需要对Mutex的可变引用。如果你持有对Mutex的可变引用,那么其他人就不会持有对它的任何引用,无论如何。由于没有其他引用,就不会出现竞争条件。因此,不需要进行锁定。 - undefined
请注意,这不是一个独立的例子,而是一个在任何具有内部可变性的地方经常出现的模式。例如,std::cell::Cell有一个方法Cell::get_mut,它允许您获取对单元格内项目的可变引用,只要您拥有对单元格本身的“独占”引用。 - undefined
2
只是为了明确一下,你是指像这样的playground吗?你认为之所以存在未定义行为是因为在最后一个_release_和get_mut()的访问之间没有_acquire_,对吗? - undefined
2
@AntonDyachenko 你能写一个展示UB的MRE吗? - undefined
@AntonDyachenko "Rust借用检查器对线程和竞态条件一无所知" -> 是的,它知道的,可以查看SyncSend特性。 - undefined
显示剩余2条评论
3个回答

6
可变引用在Rust中的主要特性是它们是唯一的。也就是说,当整个代码是正确的时候,保证通过这个精确的引用是访问可变引用所引用的值的唯一方式。在安全代码中,这是自动强制执行的。在不安全的代码中,包括FFI,这是不变的,作为编写不安全代码的人,你需要遵守这个不变量。
特别是当你有对Mutex的可变引用时,任何其他地方访问同一个互斥锁或其后面的数据都是明确和立即的未定义行为。特别是,它基本上是未定义行为,如果它被其他人锁定,因为解锁将根据定义访问互斥锁(以存储“未锁定”位)。
因此,如果一些不安全的代码导致get_mut不是线程安全的,那么这个不安全的代码本身就是不正确的。

2
创建了两个可变引用后,不是已经是UB了吗?也就是说,实际上不需要访问任何内容。 - undefined
2
我认为OP指的是当我们不是有两个可变引用,而是例如&mut T*mut T的情况。这两者同时存在并不会导致未定义行为,但是在引用处于活动状态时使用原始指针绝对是不允许的。 - undefined
core::ptr::write,例如。 - undefined
@AntonDyachenko 使用原始指针访问共享内存而没有适当的内存屏障或其他同步机制是不可靠的。我不确定你为什么认为它是可靠的。 - undefined
@AntonDyachenko 很抱歉直言不讳地说,但是你的论点基本上是无关紧要的,因为是的,不安全的操作可能会产生未定义行为。这不是问题。但是来剖析一下:你说“在使用mut时没有其他引用”,但并没有真正定义“when”的含义(Rust缺乏内存/别名模型...)。同时性不存在,尤其是在不同核心上的进程之间。如果你从不同的角度来看:要知道是否存在其他引用,你需要同步。如果没有同步,引用实际上可能同时存在,并且这种可能性足以导致未定义行为。 - undefined
显示剩余2条评论

4
如果你只使用安全的代码,那么可变引用的存在就意味着你在编译时有一个先于关系,因此是同步的。
如果你使用不安全的代码来访问锁,那么你需要确保有一个先于关系。如果你不这样做,即使实际上没有改变数据,你的代码也是不可靠的,因为你创建了两个重叠的可变引用而没有同步,因此会产生未定义行为。所以你需要负责创建适当的同步。如果你这样做了,一切都很好。如果你不这样做,你的不安全代码就是不可靠的,未定义行为来自于你的不安全代码。std的get_mut()在这里没有任何作用,因此是可靠的。

-3
我的问题的答案是,是的,当然是不安全的。在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) {
        // 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的观点更加坚定。只有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 的内部结构包含 UnsafeCellsys::Mutex,在我提供的例子中,对 sys::Mutex 的任何操作都是线程安全的,这是由 sys::Mutex 的实现保证的,但对 UnsafeCell 的访问则需要 happens-before 关系,这就是 Mutex 的用途,不是吗?
换句话说,如果你将Mutex抽象拆解为一对sys::MutexUnsafeCell,并将指针传递给这个封装的对组,然后将指针转换回引用,你可以在所有情况下安全地使用sys::Mutex部分,但是如果没有确保先行发生关系,你不能这样做UnsafeCell
附言:基本上,所有反对这个答案的人的论点都可以归结为一个简单的句子:
在Rust中,在任何安全代码中使用std::sync::AtomicPtrstd::sync::atomic::Ordering::Relaxed是非法的。
当这一点被编码到Rust类型系统中时,我可以接受并认同这个观点。

1
不,你的代码是不稳定的,执行起来完全是未定义的行为。正如我在答案中所说的,你有两个指向同一块内存的Box这是未定义的行为。它可能在物理上不是同时发生的,但在抽象机器上它是同时发生的(因为没有同步),这才是唯一重要的事情。 - undefined
1
@alter_igel 这并不完全正确;即使一个不安全的代码只能通过不安全的代码引发未定义行为(UB),但它仍可能被认为是不可靠的,前提是该不安全的代码是可靠的。当然,可靠性的定义是传统的。有些事情显然是不可靠的,但有些则不然,可以说是可靠的或不可靠的。在这种情况下,正如我所说,根据规则,这段代码是不可靠的,但即使没有这个规则,我们通常认为标准代码是可靠的,因此我们假设不安全代码触发标准代码的UB是不可靠的。 - undefined
1
但是再说一遍,物理时间并不重要。我们不是在物理世界中运行 - 我们是在抽象机器世界中运行。在那个世界里,除非有适当的同步(先于关系),否则来自两个线程的事物会交错。 - undefined
1
假设Arc<T>中的T没有内部可变性,根据Rust语言的保证,使用放松顺序对计数器进行操作始终是安全的,因为对受保护数据的唯一访问是加载操作。而且加载操作必须在释放内存之前发生(我们不希望出现使用已释放内存的情况,对吗?)。那么,如何在不要求在原子屏障之前进行加载操作的情况下保证这一点呢? - undefined
1
此外,关于Drop的所有讨论与关于get_mut()的讨论无关。也许Mutex::drop()是错误的(不是完全错误,只是稍微有些错误)。get_mut()仍然是正确的。即使我们希望get_mut()能够防止数据竞争,它也无法做到。因为它接收到的是一个&mut引用,正如我已经解释过的,即使在非同步的上下文中存在一个&mut引用本身就是明显的未定义行为,甚至在调用get_mut()之前(或者没有调用)也是如此。 - undefined
显示剩余20条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接