Rust中使用Tokio Mutex遭遇死锁问题求助
嗨,刚接触Rust异步并发遇到这种死锁问题太正常了,我来帮你拆解一下问题根源和解决办法~
问题根源:Tokio Mutex不支持重入
先理清楚你的代码执行流程,死锁的原因一下子就明了了:
- 在
callback_query函数里,你先通过let hero_mut = hero.lock().await;拿到了hero这个Arc<Mutex<Hero>>的锁 - 接着你把同一个
Arc<Mutex<Hero>>的克隆(hero_clone)传给了ability.perform方法 - 在
perform方法里,你又调用hero.lock().await尝试再次获取锁
这里的核心坑点是:Tokio的tokio::sync::Mutex是不支持重入的。也就是说,同一个异步任务(task)如果已经持有了某个Mutex的锁,再次请求锁会导致当前任务被挂起,等待锁被释放——但持有锁的就是当前这个被挂起的任务,它根本没法释放锁,于是就陷入了永远等待的死锁状态。
解决办法
针对你的代码场景,有两个直接可行的解决方案,你可以根据代码结构选择:
方案1:复用已有的锁,避免重复请求
既然你在callback_query里已经拿到了hero的锁,完全可以直接把已锁定的Hero引用传给perform方法,不需要再在perform里重新加锁:
首先修改Ability::perform的签名,直接接受&mut Hero:
impl Ability { async fn perform(&self, bot: &Bot, game: Arc<Mutex<Game>>, target: Arc<Mutex<Player>>, hero: &mut Hero) -> bool { hero.damage(40); // 尽量缩小锁的持有范围,拿到需要的id后就释放Game的锁 let game_id = game.lock().await.id; hero.send_message(bot, &game_id, "Take the khafang punchoo!".to_string()).await; true } }
然后在callback_query里,直接把已锁定的hero_mut的引用传进去:
async fn callback_query(bot: Bot, callback_query: CallbackQuery) -> HandlerResult { // ... 省略其他代码 ... let mut hero_mut = hero.lock().await; let ability = hero_mut.abilities.get(ability).unwrap(); let opponent = game_mut.get_next_turn(); // 直接传&mut hero_mut,不需要再传hero_clone了 ability.perform(&bot, game.clone(), opponent, &mut hero_mut).await; game_mut.next_turn(&bot).await; // ... 省略其他代码 ... }
这样既解决了死锁问题,还能缩小锁的持有范围,提升并发效率。
方案2:改用支持重入的异步Mutex
如果你的代码结构实在需要在同一个任务里多次获取同一个锁,可以改用Tokio提供的ReentrantMutex(可重入锁),它允许同一个任务多次获取锁而不会死锁:
首先把你的Heroes类型定义里的Mutex换成ReentrantMutex:
use tokio::sync::ReentrantMutex; pub type Heroes = [Arc<ReentrantMutex<Hero>>; 1];
ReentrantMutex的使用方式和Mutex几乎一致,但注意:可重入锁虽然能解决当前问题,但会掩盖一些设计上的冗余锁请求,建议优先用方案1,只有万不得已的时候再用重入锁。
额外小提醒
Rust异步里的Mutex和同步的std::sync::Mutex差异不小,比如Tokio的Mutex是为异步任务设计的,不能在阻塞代码里使用,而且默认不重入这个特性很容易踩坑。平时可以多看看Tokio官方文档里的并发章节,能帮你少踩很多这类新手坑~
备注:内容来源于stack exchange,提问作者Mmd

