【问题标题】:C# thread-safe atomic operation on item in ConcurrentDictionary对 ConcurrentDictionary 中项目的 C# 线程安全原子操作
【发布时间】:2019-12-16 03:59:53
【问题描述】:

如何对键可能在也可能不在 ConcurrentDictionary 中的项执行原子操作?

例如,假设我想使用特定键重置项目的一些状态值,它可能尚未在字典中,也可能不在字典中。我不想重置所有状态值,因此不能简单地用新实例替换原始项目。

这是一个与我的用例非常相似的人为示例。您可以假设类和方法包含更多不会影响手头问题的代码。

public class MyManager
{
    private ConcurrentDictionary<int, MyWorker> _dict
        = new ConcurrentDictionary<int, MyWorker>();

    public void DoSomething1()
    {
        // ...
        _dict.GetOrAdd(1, new MyWorker(Guid.NewGuid()));
        // ...
    }

    public void DoSomething2()
    {
        // ...
        _dict.TryRemove(1, out MyWorker worker);
        // ...
    }

    public void DoSomething3()
    {
        // OPTION 1: Won't it be possible for a different thread to remove
        // the object before the Reset() call?
        var myWorker = _dict.GetOrAdd(1, new MyWorker(Guid.NewGuid()));
        myWorker.Reset();
        // OPTION 2: Does method chaining somehow guarantee execution within
        // the same lock?
        _dict.GetOrAdd(1, new MyWorker(Guid.NewGuid())).Reset();
        // OPTION 3: ?
    }
}

public class MyWorker
{
    private int _someValue = 1;
    private IList<string> _someList = new List<string>();
    private Guid _immutableValue; // should not get reset

    public MyWorker(Guid immutableValue)
    {
        _immutableValue = immutableValue;
    }

    public void Reset()
    {
        _someValue = 1;
        _someList = new List<string>();
        // NB. do not change _immutableValue!
    }
}

在此示例中,不同的线程可能会以任意顺序多次调用 DoSomething1() 或 DoSomething2(),这可能会导致字典中键为“1”的 MyWorker 随时被替换。

我需要 DoSomething3() 来调用 MyWorker.Reset() 并知道如果字典中已经存在该对象,则将使用该对象。

  1. 选项 1 允许在调用 Reset 之前将检索到的 MyWorker 替换为新实例。
  2. 选项 2 似乎会做同样的事情,除非在流畅使用时有特殊处理?

还有哪些其他选项可以检索一个值并在释放锁之前检索它的同一个锁中对其执行方法/操作?换句话说,如何保证原子操作?我可以以某种方式传入 lambda 吗?

【问题讨论】:

  • 听起来这可能是一个有趣的问题,但我不知道你实际问的是什么。您能否解释一下您正在解决什么问题以及出了什么问题?
  • 你签出AddOrUpdate了吗?
  • @yaakov AddOrUpdate 似乎没有重载,它允许在添加/更新对象的同一锁定范围内对添加/更新对象运行方法。唯一可用的重载允许自定义工厂生成唯一键值。
  • @Enigmativity 我需要对ConcurrentDictionary 中的一个项目进行操作(在示例中,调用Reset())。如果我获得了对该项目的引用,我只能拨打Reset() 作为第二次呼叫。在多线程运行时环境中,其他一些线程可以在检索之后但在调用Reset() 之前更新甚至删除项目。最终结果是 Reset() 可能无法按预期执行,因为它正在对一个对象进行操作,该对象要么:a)被替换(我只有对旧对象的引用),要么 b)以现在的方式更改拨打电话不合适。
  • @Mellevsen - 这不是您要解决的问题,而是您为解决问题而编写的代码。您的业​​务问题是什么?

标签: c# concurrency thread-safety atomic


【解决方案1】:

ConcurrentDictionary 是线程安全的,因为它可以保护其内部状态免受损坏。它不能保护它作为键或值包含的对象的状态免受损坏。使用GetOrAdd 方法从ConcurrentDictionary 检索对象后,如果您想改变该对象的状态并且该对象不是线程安全的,您需要负责同步多个线程对该对象的访问,方法是使用锁或其他方式。换句话说,ConcurrentDictionary 类提供的线程安全保证仅限于自身。

您可能仍想使用这个类,即使它不能满足您所有的线程安全需求,因为它的细粒度内部锁定实现在高线程争用的情况下会非常有效。

【讨论】:

  • 感谢@theodor-zoulias - 你已经证实了我的怀疑,即无法在相同的锁定范围内运行代码。因此,替换集合中的项目永远不会 100% 起作用,因此我们需要在项目类(例如 MyWorker)本身内处理线程安全。
  • 是的,我注意到(来自 SO 中的问题)ConcurrentDictionary 类通常会产生比它实际能够提供的更大的期望。 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多