我如何锁定ConcurrentDictionary的元素?

时间:2018-05-24 17:01:47

标签: c# multithreading locking race-condition

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

  1. items[itemId]的引用分配给item
  2. 锁定item
  3. 不一定是原子的。

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

    1. 主题1执行item = items[itemId],但尚未执行lock(item)
    2. 线程2对lock(item = items[itemId])
    3. 的相同值执行itemId
    4. 主题2从itemId
    5. 中删除items
    6. 线程2释放锁定
    7. 线程1执行lock(item),不知道itemId已不在词典中
    8. 线程1错误地在item上运行,而不是按原样运行到catch块。
    9. 上述分析是否正确?

      在这种情况下,以这种方式修改我的代码是否足够?

      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”。如果由于玩家移动游戏完成,那么我们return the final state of the game并从字典中移除游戏。当然,对完成的游戏进行任何进一步的移动是无效的,因此是try / catch块。

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

      • 玩家发送无效命令,命令在不存在的游戏中进行移动;
      • 由于网络滞后,玩家在有效游戏上移动的命令会在计时器用完后到达服务器。

1 个答案:

答案 0 :(得分:0)

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

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