如果 strong_count 为 1 且 weak_count 为 0,则包含 `Rc` 的 `Send` 结构是否安全?

Is it safe to `Send` struct containing `Rc` if strong_count is 1 and weak_count is 0?

我有一个不是 Send 的结构,因为它包含 Rc。可以说 Arc 的开销太大,所以我想继续使用 Rc。我仍然想偶尔在线程之间 Send 这个结构,但只有当我可以验证 Rc 有 strong_count 1 和 weak_count 0.

这是我想到的(希望是安全的)抽象:

mod my_struct {
    use std::rc::Rc;

    #[derive(Debug)]
    pub struct MyStruct {
        reference_counted: Rc<String>,
        // more fields...
    }

    impl MyStruct {
        pub fn new() -> Self {
            MyStruct {
                reference_counted: Rc::new("test".to_string())
            }
        }

        pub fn pack_for_sending(self) -> Result<Sendable, Self> {
            if Rc::strong_count(&self.reference_counted) == 1 &&
               Rc::weak_count(&self.reference_counted) == 0
            {
                Ok(Sendable(self))
            } else {
                Err(self)
            }
        }

        // There are more methods, some may clone the `Rc`!
    }

    /// `Send`able wrapper for `MyStruct` that does not allow you to access it,
    /// only unpack it.
    pub struct Sendable(MyStruct);

    // Safety: `MyStruct` is not `Send` because of `Rc`. `Sendable` can be
    //         only created when the `Rc` has strong count 1 and weak count 0.
    unsafe impl Send for Sendable {}

    impl Sendable {
        /// Retrieve the inner `MyStruct`, making it not-sendable again.
        pub fn unpack(self) -> MyStruct {
            self.0
        }
    }
}

use crate::my_struct::MyStruct;

fn main() {
    let handle = std::thread::spawn(|| {
        let my_struct = MyStruct::new();
        dbg!(&my_struct);

        // Do something with `my_struct`, but at the end the inner `Rc` should
        // not be shared with anybody.

        my_struct.pack_for_sending().expect("Some Rc was still shared!")
    });

    let my_struct = handle.join().unwrap().unpack();
    dbg!(&my_struct);
}

我在 Rust playground 上做了一个演示。

有效。我的问题是,它真的安全吗?

我知道 Rc 只属于一个人,在我手下没有人可以更改它,因为它不能被其他线程访问,我们将它包装到 Sendable不允许访问包含的值。

但是在一些疯狂的世界中 Rc 可以在内部使用线程本地存储,这并不安全...那么是否可以保证我可以做到这一点?

我知道我必须非常小心,不要引入一些额外的理由让 MyStruct 不是 Send

ArcRc 之间的唯一区别是 Arc 使用原子计数器。只有在克隆或删除指针时才会访问计数器,因此在仅在长期线程之间共享指针的应用程序中,两者之间的差异可以忽略不计。

如果您从未克隆过 Rc,在线程之间发送是安全的。但是,如果您可以 保证 指针是唯一的,那么您可以对原始值做出相同的保证,而根本不需要使用智能指针!

这一切似乎都很脆弱,没有什么好处;未来对代码的更改可能不符合您的假设,您最终会遇到未定义的行为。我建议您至少尝试使用 Arc 进行一些基准测试。只有在衡量性能问题时才考虑这样的方法。


您也可以考虑使用 archery crate,它提供了一个对原子性进行抽象的引用计数指针。

没有

有多个点需要验证才能跨线程发送Rc

  1. 不能有其他句柄(RcWeak)共享所有权。
  2. Rc的内容必须是Send
  3. Rc 的实现必须使用线程安全策略。

让我们按顺序回顾一下!

保证没有锯齿

虽然您的算法(您自己检查计数)目前有效,但最好简单地询问 Rc 它是否有别名。

fn is_aliased<T>(t: &mut Rc<T>) -> bool { Rc::get_mut(t).is_some() }

如果 Rc 的实施以您未曾预见的方式发生变化,get_mut 的实施将进行调整。

可发送内容

虽然您的 MyStruct 实施目前将 String(即 Send)放入 Rc,但明天可能会更改为 Rc<str>,然后所有投注均已取消。

因此,可发送检查需要在 Rc 级别本身实施,否则您需要审核对 Rc 持有的任何内容的任何更改。

fn sendable<T: Send>(mut t: Rc<T>) -> Result<Rc<T>, ...> {
    if !is_aliased(&mut t) {
        Ok(t)
    } else {
        ...
    }
}

线程安全Rc内部

而且...无法保证。

由于Rc不是Send,其实现可以通过多种方式进行优化:

  • 可以使用线程局部区域分配整个内存。
  • 计数器可以使用线程局部区域单独分配,以便无缝转换 to/from Box.
  • ...

据我所知,目前情况并非如此,但是 API 允许这样做,因此下一个版本肯定会利用这一点。


你该怎么办?

您可以 pack_for_sending unsafe,并尽职地记录所有依赖的假设——我建议使用 get_mut 删除其中之一。然后,在 Rust 的每个新版本上,您必须仔细检查每个假设以确保您的使用仍然安全。

或者,如果您不介意进行分配,您可以自己编写一个到 Arc<T> 的转换(参见 Playground):

fn into_arc(this: Rc) -> 结果 { Rc::try_unwrap(this).map(|t| Arc::new(t)) }

或者,您可以编写一个 RFC 提议 Rc <-> Arc 转换!

API 将是:

fn Rc<T: Send>::into_arc(this: Self) -> Result<Arc<T>, Rc<T>>

fn Arc<T>::into_rc(this: Self) -> Result<Rc<T>, Arc<T>>

这可以在 std 内部非常有效地完成,并且可以对其他人有用。

然后,您将从 MyStruct 转换为 MySendableStruct,只需移动字段并将 Rc 转换为 Arc,发送到另一个线程,然后转换回 MyStruct.

而且您不需要任何 unsafe...