如何在Tokio应用中借用HashMap存储的值给函数
解决方案:用
Arc<tokio::sync::Mutex<Game>>实现共享可变访问 你的核心问题是需要让GameManager的HashMap和game_loop任务共享同一个Game实例。克隆会产生独立副本,自然无法同步状态,而Arc<Mutex>正是Rust中跨线程/任务共享可变数据的标准方案,结合Tokio异步环境,我们需要用Tokio提供的异步Mutex(避免阻塞线程)。
步骤1:调整GameManager的结构体定义
把games的类型从HashMap<Uuid, Game>改为HashMap<Uuid, Arc<tokio::sync::Mutex<Game>>>,这样每个Game都被包装在原子引用计数的异步互斥锁中:
use tokio::sync::Mutex; use std::sync::Arc; pub struct GameManager { games: HashMap<Uuid, Arc<Mutex<Game>>>, game_channels: HashMap<Uuid, UnboundedSender<GameEvent>>, broadcast: UnboundedSender<ServerEvent>, }
步骤2:修改find_or_create_game方法
创建Game后,用Arc::new(Mutex::new(game))包装,插入HashMap的同时,克隆一份Arc传给game_loop(Arc的克隆是轻量的,只是增加引用计数):
impl GameManager { async fn find_or_create_game(&mut self, user_options: &GameOptions) -> Uuid { // 遍历查找可加入的游戏,需要异步获取锁访问Game方法 for game_lock in self.games.values() { let game = game_lock.lock().await; println!("game id {:?}", game.id); println!("game players {:?}", game.state.get_players()); if game.allows_joining() && game.matches_player_options(user_options) { println!("Joining existing game"); return game.id; } } // 创建新游戏 let rng = ::rand::rngs::StdRng::from_seed(OsRng.gen()); let game = Game::new(Some(user_options.clone()), rng); let game_id = game.id; let (game_sender, game_receiver) = mpsc::unbounded_channel::<GameEvent>(); let broadcast = self.broadcast.clone(); // 包装Game并插入HashMap let game_arc = Arc::new(Mutex::new(game)); self.game_channels.insert(game_id, game_sender.clone()); self.games.insert(game_id, game_arc.clone()); // 传给game_loop的是Arc的克隆 tokio::spawn(game_loop(game_arc, broadcast, game_receiver)); game_id } }
步骤3:修改game_loop函数
将参数从mut game: Game改为game_arc: Arc<Mutex<Game>>,每次需要访问或修改Game时,通过lock().await获取可变引用:
pub async fn game_loop( game_arc: Arc<Mutex<Game>>, broadcast: UnboundedSender<ServerEvent>, mut receiver: UnboundedReceiver<GameEvent>, ) -> Result<(), io::Error> { // 先获取一次Game计算帧间隔 let dur = { let game = game_arc.lock().await; std::time::Duration::from_secs_f64(1.0 / game.state.options.fps as f64) }; let mut interval = tokio::time::interval(dur); loop { interval.tick().await; // 处理事件:每次处理都需要锁Game while let Some(event) = receiver.recv().await { let mut game = game_arc.lock().await; handle_game_event(event, &mut game, &broadcast); } // 检查游戏状态并执行tick let mut game = game_arc.lock().await; if game.has_ended() { break; } else { game.tick(); let _ = broadcast.send(ServerEvent::Tick(game.get_tick())); } } Ok(()) }
关键注意事项
- 使用Tokio的Mutex:
tokio::sync::Mutex是为异步场景设计的,lock()返回Future,通过await等待不会阻塞线程;而std::sync::Mutex在异步环境中会导致线程阻塞,必须避免。 - 锁的粒度:尽量缩小持有锁的范围,比如计算帧间隔时用代码块限制锁的持有时间,减少锁竞争。
- 引用计数:
Arc的克隆是原子操作,线程安全,适合跨任务共享;当最后一个Arc被销毁时,Game才会被自动释放。
这样修改后,GameManager的HashMap和game_loop任务就共享同一个Game实例了,两边的修改都会同步到同一数据上。
内容的提问来源于stack exchange,提问作者TeemuK
相关产品推荐
相关产品推荐

