如何将对堆栈变量的引用传递给线程?

How can I pass a reference to a stack variable to a thread?

提问人:Ned Ruggeri 提问时间:9/24/2015 最后编辑:applemonkey496Ned Ruggeri 更新时间:7/14/2022 访问量:16111

问:

我正在编写一个 WebSocket 服务器,其中 Web 客户端连接到与多线程计算机 AI 下棋。WebSocket 服务器希望将对象传递到 AI 代码中。该对象将通过管道将日志行从 AI 向下传递到 Web 客户端。必须包含对客户端连接的引用。LoggerLoggerLogger

我对生存期如何与线程交互感到困惑。我已经用一个由类型参数化的结构重现了这个问题。该函数尝试解开该值并记录它。Wrapperrun_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 });
}

参数存在于堆栈上,其生存期不会超过 的堆栈帧,即使线程将在堆栈帧结束之前加入。我可以从堆栈中复制该值:wrapperrun_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]);
}

在我的实际程序中,似乎 和 connection 对象都必须放在包装器中。当代码并行化的库内部时,客户端需要将连接装箱,这似乎很烦人。这尤其令人讨厌,因为连接的生存期保证大于工作线程的生存期。LoggerArcArc

我错过了什么吗?

多线程 Rust 参考 生存期

评论


答:

73赞 Shepmaster 9/24/2015 #1

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

一种解决方案是作用域线程,即保证在父线程退出之前退出的线程。这些可以确保父线程中的堆栈变量在线程的整个持续时间内都可用。

锈 1.63

std::thread::scope 在中断 7 年后返回稳定的 Rust(删除返回)。

use std::{thread, time::Duration};

fn main() {
    let mut vec = vec![1, 2, 3, 4, 5];

    thread::scope(|scope| {
        for e in &mut vec {
            scope.spawn(move || {
                thread::sleep(Duration::from_secs(1));
                *e += 1;
            });
        }
    });

    println!("{:?}", vec);
}

早期的 Rust 版本或需要更多控制时

横梁

我们不局限于标准库;一个流行的带范围螺纹的板条箱是横梁

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);
}

关于示例

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

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


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

也许你可以更好地隐藏你的并行性呢?你能接受记录器,然后在把它交给你的线程之前把它包装在/中吗?ArcMutex

评论

1赞 Ned Ruggeri 9/25/2015
非常感谢您的回复!我的解决方案是制作实现,并有一个类型为 .然后,用户可以将记录器的克隆传递给线程代码。用户无法将 的所有权转让给线程代码(用户需要它用于其他目的),因此我看不出线程代码可以方便地代表用户执行 and 装箱。LoggerCloneArc<Mutex<Connection>>ConnectionArc
1赞 Brandon Ros 7/3/2020
如果你尝试传递的变量无法实现克隆/复制,你会怎么做?就像 rusb crate 中的 USB 设备句柄一样
0赞 Shepmaster 7/3/2020
@BrandonRos 不实现,也不在这些代码示例中使用。此处提供的代码适用于此类类型。VecCopyClone