【问题标题】:How may I lock an element of a ConcurrentDictionary?如何锁定 ConcurrentDictionary 的元素?
【发布时间】:2018-05-24 17:01:47
【问题描述】:

我有一个ConcurrentDictionary 存储Items:

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 对 itemId 的相同值执行 lock(item = items[itemId])
  3. 线程 2 从 items 中删除 itemId
  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”。如果由于玩家的移动游戏完成,那么我们return the final state of the game 并从字典中删除游戏。当然,在已完成的游戏上执行任何进一步的移动都是无效的,因此是 try/catch 块。

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

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

【问题讨论】:

  • ConcurrentDictionary 的重点不就是你不需要需要锁语句吗?
  • @maccettura AFAIK ConcurrentDictionary 意味着我可以从/到这个字典中自动检索/添加元素(所以我不必锁定整个字典来访问或添加元素)。但是如果我想对任何元素进行操作,我仍然需要锁定这个特定的元素。我错了吗?
  • 不仅可以检索,还可以从列表中添加/删除项目。 - 我会做以下使用TryRemove检查返回值并使用out参数做一些事情,当你完成你的操作时,如果需要,重新添加它
  • @RandRandom 不确定我是否理解你。根据您的建议,当线程 1 对某个项目进行操作时,线程 2 不会错误地看到该项目不在字典中,而是正确地等待线程 1 完成?
  • 既然你在做一个国际象棋游戏,怎么可能在一个游戏对象上同时发生 2 个或多个操作呢?国际象棋是回合制游戏,因此在任何给定时间只有一名玩家可以与游戏互动,因此基于该信息 - 我相信您所做的一切都是不必要的。所以,我更想说的是,当轮不到他们的时候,你的客户端不应该能够向服务器发送操作。

标签: c# multithreading locking race-condition


【解决方案1】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-29
    • 2012-09-07
    • 1970-01-01
    • 2010-12-29
    • 1970-01-01
    • 1970-01-01
    • 2016-08-20
    相关资源
    最近更新 更多