我有一个ConcurrentDictionary
Item
s:
ConcurrentDictionary<ulong, Item> items;
现在我想锁定这本字典中的Item
,以便我可以安全地对其进行操作。
这段代码是否正确?
try
{
Item item;
lock(item = items[itemId])
{
// do stuff
// possibly remove itemId from the dictionary
}
}
catch(KeyNotFoundException)
{
// ...
}
我担心的是这个。我想lock(item = items[itemId])
可以分解为两个操作:
- 将
items[itemId]
的引用分配给item
- 锁定
item
这不一定是原子的。
所以,我担心以下竞争条件:
- 线程 1 执行
item = items[itemId]
,但尚未执行lock(item)
- 线程 2 对相同的
itemId
值执行lock(item = items[itemId])
- 线程 2 擦除
itemId
items
- 线程 2 释放其锁
- 线程 1 执行
lock(item)
,不知道itemId
不再在字典中 - 线程 1 在
item
上错误地运行,而不是像它应该的那样进入它的catch
块。
上述分析正确吗?
在这种情况下,以这种方式修改我的代码是否足够?
try
{
Item item;
lock(items[itemId])
{
item = items[itemId];
// do stuff
// possibly remove itemId from the dictionary
}
}
catch(KeyNotFoundException)
{
// ...
}
编辑:因为我开始假设我已经爱上了XY问题。这是背景。
多人国际象棋游戏。itemId
是游戏的 ID。item
是游戏的当前状态。字典包含正在进行的项目。该操作是处理玩家的移动,例如"来自e3的骑士转到d1"。如果由于玩家的移动导致游戏完成,则我们返回游戏的最终状态并从字典中删除游戏。当然,在已完成的游戏中执行任何进一步的动作都是无效的,因此 try/catch 块。
try/catch 块应该正确检测以下情况:
- 玩家发送无效命令,命令在不存在的游戏中采取行动;
- 由于网络滞后,玩家在计时器用完后,对有效游戏进行移动的命令会到达服务器。
您更新的代码也好不到哪里去。 线程仍然可以从字典中获取值,而不是锁定,然后在锁被取出之前让另一个线程删除该项目。
从根本上说,您希望代码是原子的,而不仅仅是对ConcurrentDictionary
的单个调用,因此您只需要执行自己的锁定并使用常规字典。
这里的主要问题源于您尝试锁定可能存在也可能不存在或可能正在更改的对象。 这只会给你带来各种各样的问题。 另一种解决方案是不这样做,并确保字典不会更改或删除键的值,以便您可以安全地锁定它。 一种方法是创建一个包装器对象:
public static void Foo(ConcurrentDictionary<ulong, ItemWrapper> items, ulong itemId)
{
if (!items.TryGetValue(itemId, out ItemWrapper wrapper))
{
//no item
}
lock (wrapper)
{
if (wrapper.Item == null)
{
//no actual item
}
else
{
if (ShouldRemoveItem(wrapper.Item))
{
wrapper.Item = null;
}
}
}
}