【问题标题】:How can I pass a reference to a stack variable to a thread?如何将堆栈变量的引用传递给线程?
【发布时间】:2015-12-21 10:27:16
【问题描述】:

我正在编写一个 WebSocket 服务器,一个 Web 客户端连接到该服务器以与多线程计算机 AI 下棋。 WebSocket 服务器想要将 Logger 对象传递给 AI 代码。 Logger 对象将把日志行从 AI 传送到 Web 客户端。 Logger 必须包含对客户端连接的引用。

我对生命周期如何与线程交互感到困惑。我用一个类型参数化的Wrapper 结构重现了这个问题。 run_thread 函数尝试解开该值并记录它。

use std::fmt::Debug;
use std::thread;

struct Wrapper<T: Debug> {
    val: T,
}

fn run_thread<T: Debug>(wrapper: Wrapper<T>) {
    let thr = thread::spawn(move || {
        println!("{:?}", wrapper.val);
    });

    thr.join();
}

fn main() {
    run_thread(Wrapper::<i32> { val: -1 });
}

wrapper 参数存在于堆栈中,并且它的生命周期不会超过 run_thread 的堆栈帧,即使线程将在堆栈帧结束之前加入。我可以从堆栈中复制值:

use std::fmt::Debug;
use std::thread;

struct Wrapper<T: Debug + Send> {
    val: T,
}

fn run_thread<T: Debug + Send + 'static>(wrapper: Wrapper<T>) {
    let thr = thread::spawn(move || {
        println!("{:?}", wrapper.val);
    });

    thr.join();
}

fn main() {
    run_thread(Wrapper::<i32> { val: -1 });
}

如果 T 是对我不想复制的大对象的引用,这将不起作用:

use std::fmt::Debug;
use std::thread;

struct Wrapper<T: Debug + Send> {
    val: T,
}

fn run_thread<T: Debug + Send + 'static>(wrapper: Wrapper<T>) {
    let thr = thread::spawn(move || {
        println!("{:?}", wrapper.val);
    });

    thr.join();
}

fn main() {
    let mut v = Vec::new();
    for i in 0..1000 {
        v.push(i);
    }

    run_thread(Wrapper { val: &v });
}

结果:

error: `v` does not live long enough
  --> src/main.rs:22:32
   |
22 |     run_thread(Wrapper { val: &v });
   |                                ^ does not live long enough
23 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...

我能想到的唯一解决方案是使用Arc

use std::fmt::Debug;
use std::sync::Arc;
use std::thread;

struct Wrapper<T: Debug + Send + Sync + 'static> {
    arc_val: Arc<T>,
}

fn run_thread<T: Debug + Send + Sync + 'static>(wrapper: &Wrapper<T>) {
    let arc_val = wrapper.arc_val.clone();
    let thr = thread::spawn(move || {
        println!("{:?}", *arc_val);
    });

    thr.join();
}

fn main() {
    let mut v = Vec::new();
    for i in 0..1000 {
        v.push(i);
    }

    let w = Wrapper { arc_val: Arc::new(v) };
    run_thread(&w);

    println!("{}", (*w.arc_val)[0]);
}

在我的实际程序中,似乎Logger 和连接对象都必须放在Arc 包装器中。当代码被并行化的库内部时,客户端需要将连接装箱Arc 似乎很烦人。这特别烦人,因为连接的生命周期肯定会比工作线程的生命周期长。

我错过了什么吗?

【问题讨论】:

    标签: multithreading rust reference lifetime


    【解决方案1】:

    标准库中的线程支持允许创建的线程比创建它们的线程寿命更长;这是好事!但是,如果要将堆栈分配变量的引用传递给这些线程之一,则无法保证该变量在线程执行时仍然有效。在其他语言中,这将允许线程访问无效内存,从而产生一堆内存安全问题。

    幸运的是,我们并不局限于标准库。至少有两个 crate 提供 作用域线程——保证在某个作用域结束之前退出的线程。这些可以确保堆栈变量在线程的整个持续时间内都可用:

    还有一些 crate 可以抽象出“线程”的低级细节,但可以让您实现目标:

    以下是每个示例。每个示例都生成多个线程并在没有锁定、没有Arc 和没有克隆的情况下改变一个本地向量。请注意,突变有一个sleep 调用,以帮助验证调用是否并行发生。

    您可以扩展示例以共享对实现Sync 的任何类型的引用,例如MutexAtomic*。但是,使用这些会引入锁定。

    横梁

    use crossbeam; // 0.6.0
    use std::{thread, time::Duration};
    
    fn main() {
        let mut vec = vec![1, 2, 3, 4, 5];
    
        crossbeam::scope(|scope| {
            for e in &mut vec {
                scope.spawn(move |_| {
                    thread::sleep(Duration::from_secs(1));
                    *e += 1;
                });
            }
        })
        .expect("A child thread panicked");
    
        println!("{:?}", vec);
    }
    

    人造丝

    use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; // 1.0.3
    use std::{thread, time::Duration};
    
    fn main() {
        let mut vec = vec![1, 2, 3, 4, 5];
    
        vec.par_iter_mut().for_each(|e| {
            thread::sleep(Duration::from_secs(1));
            *e += 1;
        });
    
        println!("{:?}", vec);
    }
    

    当代码并行化在库内部时,客户端需要将连接装箱到Arc

    也许你可以更好地隐藏你的并行性?您能否接受记录器,然后将其包装在 Arc / Mutex 中,然后再将其交给您的线程?

    【讨论】:

    • 非常感谢您的回复!我的解决方案是让Logger 实现Clone,并有一个类型为Arc&lt;Mutex&lt;Connection&gt;&gt; 的字段。然后用户可以将记录器的克隆传递给线程代码。用户不能将Connection 的所有权转移给线程代码(用户需要它用于其他目的),所以我看不到线程代码可以方便地执行Arc 和装箱代表用户。
    • 如果你试图传递的变量不能实现克隆/复制,你会怎么做?就像 rusb crate 中的 USB 设备句柄
    • @BrandonRos Vec 没有实现 CopyClone 在这些代码示例中没有使用。此处提供的代码适用于此类类型。
    猜你喜欢
    • 2014-10-17
    • 2018-11-03
    • 1970-01-01
    • 1970-01-01
    • 2012-10-15
    • 2021-12-04
    相关资源
    最近更新 更多