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

63
我正在编写一个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中,这似乎很麻烦,因为连接的生命周期保证大于工作线程的生命周期,而且它是库内部的。我错过了什么吗?
1个回答

73

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

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

Rust 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版本或当您需要更多的控制时

crossbeam

我们并不局限于标准库;一个流行的用于作用域线程的crate是crossbeam

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

人造丝

还有像rayon这样的货箱,它们抽象化了"线程"的低级细节,但允许您完成目标:

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和不进行克隆的情况下就地修改本地向量。请注意,该变异具有一个sleep调用,以帮助验证这些调用是否是并行发生的。

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


当连接是库内部并行化代码时,客户端需要将连接封装在Arc中。

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


1
非常感谢您的回复!我的解决方案是让 Logger 实现 Clone,并拥有一个类型为 Arc<Mutex<Connection>> 的字段。然后用户可以将 logger 的克隆传递给线程化代码。用户无法将 Connection 的所有权转移给线程化代码(用户需要它用于其他目的),因此我认为线程化代码无法方便地代表用户执行 Arc 和装箱操作。 - Ned Ruggeri
1
如果您正在尝试传递的变量无法实现Clone/Copy,例如rusb crate中的USB设备句柄,该怎么办? - Brandon Ros
@BrandonRos Vec 没有实现 Copy,而这些代码示例中也没有使用 Clone。对于这样的类型,这里呈现的代码可以正常工作。 - Shepmaster

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