首页 > 解决方案 > 如何锁定 ConcurrentDictionary 的元素?

问题描述

我有一个ConcurrentDictionarystoing Item

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])可以分解为两个操作:

  1. 分配参考items[itemId]item
  2. 锁上item

这不一定是原子的。

所以,我担心以下竞争条件:

  1. 线程 1 执行item = items[itemId],但尚未执行lock(item)
  2. 线程 2 执行lock(item = items[itemId])相同的值itemId
  3. 线程 2itemIditems
  4. 线程 2 释放它的锁
  5. 线程1执行lock(item),不知道itemId不再在字典中
  6. 线程 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是游戏的当前状态。dict 包含正在进行的项目。该操作是处理玩家的移动,例如“来自e3的骑士到d1”。如果由于玩家的移动游戏完成,那么我们返回游戏的最终状态并将游戏从字典中删除。当然,在已完成的游戏上执行任何进一步的移动都是无效的,因此使用 try/catch 块。

try/catch 块应该正确检测以下情况:

标签: c#multithreadinglockingrace-condition

解决方案


您更新的代码也好不到哪里去。一个线程仍然有可能从字典中获取值,而不是锁定,然后让另一个线程在锁定被取出之前删除该项目。

从根本上说,您希望代码是原子的,而不仅仅是对您的单次调用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;
            }
        }
    }
}

推荐阅读