如何正确锁定ConcurrentDictionary中的元素?代码及竞态条件分析
ConcurrentDictionary中锁定单个Item的线程安全问题
你的竞态条件分析完全正确!第一个代码确实存在严重的线程安全漏洞,而你修改后的代码能解决核心问题,但还有优化空间。结合你多人象棋游戏的场景,我来详细拆解:
为什么初始代码会出问题
你提到的lock(item = items[itemId])确实是两个非原子操作:
- 第一步:从
ConcurrentDictionary中取出itemId对应的Item引用,赋值给局部变量item - 第二步:对这个
item引用加锁
这两步之间的时间窗口会触发你描述的竞态:线程1拿到了游戏状态的引用但还没加锁,线程2就拿到同一个引用、加锁、把该游戏从字典中移除并释放锁。等线程1再对这个已经被移除的游戏状态加锁时,它完全不知道字典里已经没有这个游戏了,会错误地继续操作旧状态,而不是进入catch块处理无效指令——这在象棋游戏里会导致玩家的走棋被应用到已经结束的对局上,完全不符合预期。
修改后的代码是否有效?
你修改后的代码:
try { Item item; lock(items[itemId]) { item = items[itemId]; // 处理走棋逻辑,可能移除游戏 } } catch(KeyNotFoundException) { // 处理无效操作 }
确实能解决之前的竞态问题,原因如下:
lock(items[itemId])会先执行items[itemId]获取引用,如果此时itemId已经被移除,会直接抛出KeyNotFoundException进入catch块,不会继续执行。- 即使线程1拿到引用后、加锁前,线程2把
itemId从字典中移除,线程1成功加锁后,执行item = items[itemId]时,这一步会因为itemId不存在抛出异常,同样进入catch块,不会错误地操作旧的游戏状态。
不过这个实现有个小缺点:ConcurrentDictionary的索引器在key不存在时抛出异常,而频繁的异常会带来额外的性能开销——尤其是在你的场景中,可能会有不少玩家延迟的无效指令。
更优的实现(基于TryGetValue)
推荐用TryGetValue代替索引器,避免异常开销,同时保留线程安全:
// 先尝试获取游戏状态,避免异常 if (items.TryGetValue(itemId, out var item)) { lock (item) { // 双重检查:确认游戏仍在字典中(防止获取引用后被其他线程移除) if (!items.TryGetValue(itemId, out var currentItem) || currentItem != item) { // 游戏已结束,处理无效操作 return; } // 处理玩家走棋逻辑 bool gameEnded = ProcessPlayerMove(item); // 如果游戏结束,从字典中移除 if (gameEnded) { items.TryRemove(itemId, out _); } } } else { // 游戏不存在,处理无效操作 }
这个实现的优势:
- 用
TryGetValue代替索引器,避免了异常带来的性能损耗。 - 锁定后再次检查游戏状态是否仍在字典中,彻底消除了“获取引用后、加锁前游戏被移除”的竞态窗口。
- 完全适配你的象棋游戏场景:处理走棋、结束时移除游戏、无效指令直接返回。
内容的提问来源于stack exchange,提问作者user4385532
相关产品推荐
相关产品推荐

