You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust多线程问题:循环中Engine所有权移动导致的编译错误

问题描述

我是多线程(thread)、异步、并行与并发领域的新手,不确定标题中'thread'的用法是否准确。近期我在开发一款四子棋(Connect 4)游戏引擎,该引擎可在控制台显示游戏状态、验证落子有效性、判断胜负等,同时实现了人类玩家、暴力搜索AI和蒙特卡洛AI。由于蒙特卡洛AI需要大量计算,且计算时间越长落子选择越优,我希望利用人类玩家思考落子的时间让蒙特卡洛AI并行计算,因此尝试用Rust的thread和mpsc通道实现多线程逻辑。

简化版实现代码

use std::thread;
use std::sync::mpsc;

struct Engine {

}

// Engine that plays the game. There are different ones in the actual game. This is for simplicity's sake
impl Engine {
    pub fn new() -> Engine {
        Engine {}
    }

    pub fn make_move(&mut self, gamestate: u32) {
        println!("Player made move: {}", gamestate + 2);
    }
}

fn main() {
    let mut player_blue = Engine::new();
    let mut player_red = Engine::new();

    for _ in 0..2 {
        let (tx_blue, rx_blue) = mpsc::channel();
        let (tx_blue_back, rx_blue_back) = mpsc::channel();

        let manager_blue = thread::spawn(move || {
            let mut counter = 0;
            loop {
                if let Ok(t) = rx_blue.try_recv() {
                    player_blue.make_move(t);
                    tx_blue_back.send(counter).unwrap();
                    break;
                }
                counter += 1;
            }
        });

        let manager_red = thread::spawn(move || {
            let mut input = String::new();

            // Reading in input from human
            println!("Please enter the important information we need:");
            match std::io::stdin().read_line(&mut input) {
                Ok(_) => (),
                Err(error) => panic!("Problem reading in input: {:?}", error),
            }

            // Matching the input
            let val = match parse_string_tuple(input.trim()) {
                Some(i) => i,
                None => 0,
            };

            // Make move
            player_red.make_move(val);

            // Tell other thread that reading in is finished and important calculations should be stopped
            tx_blue.send(0).unwrap();
        });

        manager_red.join().unwrap();
        manager_blue.join().unwrap();

        let counter = rx_blue_back.recv().unwrap();

        println!("Counter is: {}", counter);
    }
    println!("Finished");
}

fn parse_string_tuple(string: &str) -> Option<u32> {
    if let Ok(i) = string.parse() {
        Some(i)
    } else {
        None
    }
}

编译错误信息

加入游戏回合循环后,Engine实例会被移动到线程闭包中,导致后续迭代无法复用,出现如下编译错误:

error[E0382]: use of moved value: `player_blue`
  --> src/main.rs:27:42
   |
20 |     let mut player_blue = Engine::new();
   |         --------------- move occurs because `player_blue` has type `Engine`, which does not implement the `Copy` trait
...
27 |         let manager_blue = thread::spawn(move || {
   |                                          ^^^^^^^ value moved into closure here, in previous iteration of loop
...
31 |                     player_blue.make_move(t);
   |                     ----------- use occurs due to use in closure

现请教如何解决该所有权问题,是否可以从线程中回收Engine实例,或是有其他更合适的实现方案?


解决方案

核心思路:通过所有权转移回收或共享访问解决实例复用问题

在Rust中,move闭包会捕获变量的所有权并转移到线程中,线程结束后这些变量默认不会自动返回。针对你的场景,有两种常用的解决思路:

方案一:通过通道将Engine实例从线程返回

既然需要复用player_blue,可以在线程完成任务后,将player_blue通过额外的通道发送回主线程,这样每次循环结束后主线程就能重新获得实例的所有权。

修改后的关键代码如下:

fn main() {
    let mut player_blue = Engine::new();
    let mut player_red = Engine::new();

    for _ in 0..2 {
        let (tx_blue, rx_blue) = mpsc::channel();
        let (tx_blue_back, rx_blue_back) = mpsc::channel();
        // 新增通道用于返回Engine实例
        let (tx_engine_return, rx_engine_return) = mpsc::channel();

        let manager_blue = thread::spawn(move || {
            let mut counter = 0;
            loop {
                if let Ok(t) = rx_blue.try_recv() {
                    player_blue.make_move(t);
                    tx_blue_back.send(counter).unwrap();
                    // 将Engine实例发送回主线程
                    tx_engine_return.send(player_blue).unwrap();
                    break;
                }
                counter += 1;
            }
        });

        let manager_red = thread::spawn(move || {
            let mut input = String::new();

            println!("Please enter the important information we need:");
            match std::io::stdin().read_line(&mut input) {
                Ok(_) => (),
                Err(error) => panic!("Problem reading in input: {:?}", error),
            }

            let val = match parse_string_tuple(input.trim()) {
                Some(i) => i,
                None => 0,
            };

            player_red.make_move(val);
            tx_blue.send(0).unwrap();
        });

        manager_red.join().unwrap();
        manager_blue.join().unwrap();

        let counter = rx_blue_back.recv().unwrap();
        // 回收Engine实例
        player_blue = rx_engine_return.recv().unwrap();

        println!("Counter is: {}", counter);
    }
    println!("Finished");
}

这个方案完全遵循Rust的所有权规则,没有额外的同步开销,适合你的场景——每次循环中AI线程只需要独占访问Engine实例。

方案二:使用智能指针实现共享可变访问

如果需要更灵活的线程间共享(比如多个线程同时访问Engine),可以使用Arc<Mutex<Engine>>或Arc<RwLock<Engine>>来包装实例。Arc提供原子引用计数实现共享所有权,Mutex保证同一时间只有一个线程能可变访问实例。

修改后的关键代码如下:

use std::sync::{mpsc, Arc, Mutex};
// ... 其余代码不变

fn main() {
    // 用Arc<Mutex>包装Engine实例
    let player_blue = Arc::new(Mutex::new(Engine::new()));
    let mut player_red = Engine::new();

    for _ in 0..2 {
        let (tx_blue, rx_blue) = mpsc::channel();
        let (tx_blue_back, rx_blue_back) = mpsc::channel();
        // 克隆Arc,增加引用计数
        let blue_clone = Arc::clone(&player_blue);

        let manager_blue = thread::spawn(move || {
            let mut counter = 0;
            loop {
                if let Ok(t) = rx_blue.try_recv() {
                    // 锁定Mutex获得可变引用
                    let mut engine = blue_clone.lock().unwrap();
                    engine.make_move(t);
                    tx_blue_back.send(counter).unwrap();
                    break;
                }
                counter += 1;
            }
        });

        // ... 其余代码不变,player_red的处理同理如果需要共享的话

        manager_red.join().unwrap();
        manager_blue.join().unwrap();

        let counter = rx_blue_back.recv().unwrap();
        println!("Counter is: {}", counter);
    }
    println!("Finished");
}

这个方案适合需要多线程频繁访问实例的场景,但会带来一定的同步锁开销。对于你的四子棋AI场景,方案一已经足够简洁高效。

额外优化建议

  • 对于蒙特卡洛AI的计算,可以让AI线程在收到终止信号前持续迭代优化最佳落子,而非简单计数,充分利用人类玩家思考的时间。
  • 可以用select!宏替代try_recv()的循环,让代码更简洁高效,避免空循环浪费CPU资源。

内容的提问来源于stack exchange,提问作者Raoul Luqué

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 01:00:40