游戏开发中不必要强制使用Mutex的问题咨询
首先,咱们先拆解当前设计里的同步逻辑:你把整个PlayerList用Arc<Mutex>包裹起来供两个服务共享,同时每个Player又套了一层Arc<Mutex>。这里确实存在可以优化的点,不过先得明确是否存在不必要的Mutex——答案是:如果你的业务场景允许,PlayerList上的Mutex可以考虑替换成更高效的同步原语,而每个Player的Mutex在大多数游戏场景下是必要的,但需要注意锁的使用顺序避免死锁。
一、当前设计的潜在问题
Mutex的独占性限制并发读性能
目前PlayerList用的是Mutex,意味着哪怕是多个线程同时读取玩家列表(比如获取在线玩家数量、查询某个玩家是否存在),也必须排队等待独占锁,这在高并发场景下会成为性能瓶颈——毕竟游戏里读操作的频率通常远高于写操作(添加/移除玩家)。双重锁的死锁风险
假设存在这样的场景:- 线程A先锁住某个
Player的Mutex,然后尝试获取PlayerList的锁; - 线程B先锁住
PlayerList的锁,然后尝试获取同一个Player的锁;
这就会触发死锁,两个线程互相等待对方释放锁。
- 线程A先锁住某个
二、具体优化方案
1. 用RwLock替换PlayerList上的Mutex
RwLock支持多读者单写者的模型,完美适配读多写少的场景:
- 当你需要读取玩家列表(获取玩家、统计数量)时,多个线程可以同时获取读锁;
- 只有当你需要修改列表(添加、移除玩家)时,才会获取独占的写锁,阻塞其他所有读/写操作。
修改后的共享方式:
// NetworkServer和Server里的定义改成 player_list: Arc<RwLock<PlayerList>>,
对应的PlayerList操作示例(以获取玩家为例):
impl PlayerList { pub fn get_by_name(&self, name: &str) -> Option<Arc<Mutex<Player>>> { self.by_name.get(name).cloned() } // 在外部调用时: // let player_list = server.player_list.read().unwrap(); // let player = player_list.get_by_name("Alice"); }
2. 保留每个Player的Mutex,但规范锁的获取顺序
每个Player的Mutex是必要的——因为游戏中玩家的状态(比如生命值、位置、装备)需要被多个线程(比如网络线程、游戏逻辑线程)并发修改,单独给每个Player加锁可以让不同玩家的操作并行执行,避免锁住整个列表导致的性能下降。
但必须严格遵守锁的获取顺序:
- 永远先获取
PlayerList的锁(读或写),再获取单个Player的锁; - 禁止先获取
Player的锁,再尝试获取PlayerList的锁。
如果确实有需要反向获取锁的场景,使用try_lock()代替lock(),并处理获取失败的情况(比如重试或放弃操作):
// 错误示例:先锁Player再锁PlayerList let mut player = player.lock().unwrap(); let mut list = player_list.write().unwrap(); // 可能死锁 // 正确示例:先锁PlayerList再锁Player let list = player_list.read().unwrap(); let mut player = list.get_by_name("Alice").unwrap().lock().unwrap();
3. 可选:用原子类型优化Player的部分字段
如果Player的某些字段是独立的数值类型(比如生命值、金币数),可以用std::sync::atomic下的原子类型(比如AtomicI32、AtomicU64)代替整个Player加锁,进一步提升并发性能。比如:
pub struct Player { pub uuid: Uuid, pub name: String, pub health: AtomicI32, // 用原子类型代替普通i32 pub position: Mutex<(f32, f32, f32)>, // 复杂结构还是需要Mutex }
这样修改后,修改生命值时不需要锁住整个Player:
player.health.fetch_add(10, Ordering::Relaxed);
4. 移除Player时的注意事项
当你从PlayerList中移除玩家时,必须同时从by_name和by_uuid两个HashMap中删除,这个操作必须在PlayerList的写锁保护下完成,确保原子性。这样才能保证两个HashMap的一致性,并且让Player的Arc计数正确减少,当计数为0时自动释放资源。
三、总结
- 不必要的Mutex?:
PlayerList上的Mutex属于可以优化的点(换成RwLock),但每个Player的Mutex在大多数场景下是必要的; - 核心优化方向:用RwLock提升读并发,规范锁顺序避免死锁,结合原子类型优化单个字段的同步。
内容的提问来源于stack exchange,提问作者wingerse

