【问题标题】:How to freeze an Rc data structure and send it across threads如何冻结 Rc 数据结构并跨线程发送
【发布时间】:2020-08-16 06:12:56
【问题描述】:

我的应用程序分为两个阶段:

  1. 创建了一个大型数据结构,涉及大量临时对象并使用引用计数进行堆管理
  2. 将数据结构设为只读,任何仍处于活动状态的数据都将被停放,并将生成的结构发送到另一个线程进行读取。

(为了更加具体,应用程序是一个语言服务器,数据结构是处理单个文件,这些文件是单线程处理的,但其结果必须跨线程传递。)

目前,我使用Arc<T> 来管理数据结构,但由于阶段1 大且昂贵且单线程,我想将其切换为使用Rc<T>。但是Rc 既不是Send 也不是Sync 有充分的理由,除非程序中的所有内容基本上都使用线程安全原语,否则我无法发送数据结构或对它的引用。

我想推断,在第 2 阶段开始后,我们不再需要引用计数;线程1(所有者)不允许触摸引用计数,线程2(借用者)不允许克隆数据,只看它,所以它也不能触摸引用计数。我知道Rc 不会提供这组保证,因为您可以克隆Rc 给定共享引用。这种模式有安全的 API 吗?理想情况下,从第 1 阶段到第 2 阶段时,我不必复制任何数据。

这是一个玩具实现,只是为了给它添加一些代码。函数phase1() 在返回T 类型的数据结构之前会生成大量垃圾,然后在另一个线程上以只读方式在phase2() 中对其进行分析。如果将此代码中的Arc 更改为Rc,则会出现错误,因为它无法跨线程发送。

use std::sync::Arc;
use crossbeam::thread::scope; // uses the crossbeam crate for scoped threads

enum T { Nil, More(Arc<T>) }

fn phase1() -> T {
    let mut r = T::Nil;
    for i in 0..=5000 {
        r = T::Nil;
        for _ in 0..i { r = T::More(Arc::new(r)) }
    }
    r
}

fn phase2(mut t: &T) {
    let mut n = 0;
    while let T::More(a) = t {
        n += 1;
        t = a;
    }
    println!("length = {}", n); // should return length = 5000
}

fn main() {
    let r = phase1();
    scope(|s| { s.spawn(|_| phase2(&r)); }).unwrap();
}

【问题讨论】:

  • 为什么需要发送到另一个线程?从阅读本文后我可以看出,phase1 使用单个线程,然后它传递到phase2 再次使用单个线程。在我看来,它可以完全留在一个线程中而没有任何问题。
  • 这个例子过于简单了。实际上,线程 1 有其他事情要做,并且有多个线程 2 读取线程 1 的数据并将其与自己的工作相结合,在他们自己的阶段 1 版本中。示例的重点并不是证明架构的合理性以有效代码的形式勾勒出数据流。
  • 你的enum T是什么类型的数据结构?您的用例可能有一个现有的库。目前它看起来像某种形式的链表或树,所以我想知道是否可以用框替换引用计数器。
  • phase1 实际上是垃圾收集脚本语言的解释器。实物的分享还是蛮多的,所以引用计数很重要。 (甚至还有少量的循环,现在我只是处理内存泄漏,因为我不知道基于Gc 类型的现成竞技场。)
  • 我知道您正在寻找安全的解决方案,但如果没有;因为它只会在 phase1 之后被读取,所以你可以使用 unsafe 强制 Rc 实现 Sync ,根据你的逻辑这应该可以正常工作:play.rust-lang.org/…

标签: multithreading rust reference-counting


【解决方案1】:

我不太清楚你在大局中想要完成什么。我的收获是,您不想使用Arc 只是为了让数据结构可以在线程之间发送,而这只会发生一次。可以通过将类型包装在您手动实现Send 的另一种类型中来做到这一点。这真的非常不安全,因为编译器无法防范竞争条件。

use std::rc::Rc;
use std::thread::spawn;

// Foo is not Send because it contains a Rc
struct Foo {
    bar: Rc<bool>,
}

// Foowrapper is forced to be Send
struct FooWrapper {
    foo: Foo,
}
unsafe impl Send for FooWrapper {}

fn main() {
    println!("Hello, ?");

    // We can't send a Foo...
    let foo = Foo {
        bar: Rc::new(false),
    };

    // This blows everything up, and there is no
    // protection by the compiler
    // let secret_bar = Rc::clone(&foo.bar);

    // ...but we can send a FooWrapper.
    // I hereby promise that I *know* Foo is in a
    // state which is safe to be sent! I really checked
    // and no future updates in the code will harm me, ever!
    let wrap = FooWrapper { foo };
    spawn(move || {
        // Unwrap the Foo.
        let foo: Foo = wrap.foo;
        println!("{:?}", foo.bar);
    })
    .join()
    .unwrap();
}

在上面的例子中,我们发送一个包含Rc的数据结构,它不是Send。通过包裹FooWrapper,它变成Send。然而,留给我们100% 确定FooWrapper 发送到另一个线程的那一刻,内部Foo 可以安全发送。例如,如果主线程具有bar 之一的克隆(例如let secret_bar = Rc::clone(&amp;foo.bar);)并将其保留在发送点之外,则情况并非如此。这将允许两个线程不同步地删除它们的bar 版本,从而破坏Rc

【讨论】:

  • 好的,我认为这样的事情是必要的。请注意,我实际上并没有将Foo 移动到另一个线程,只是借用它(我无法在实际应用程序中移动它,因为有多个阅读器)。不过,这里持久的不安全让我很难过。只要我们不在线程 2 上克隆任何 Rc,我们就不必担心数据竞争,对吗?唯一真正的危险是,如果线程 1 在线程 2 仍在查看数据结构时丢弃了数据结构,但似乎可以在生命周期系统中使用它。
猜你喜欢
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多