在ConcurrentDictionary线程中实现的IDictionary成员是否安全?

Vin*_*cio 6 c# multithreading

在调用DoStuffToDictionary(dictionaryTwo)时,可以安全地假设方法体内的操作包括索引器,LINQ扩展方法也是线程安全的吗?

用不同的方式表达这个问题,是否会出现交叉线程异常或死锁?

var dictionaryOne = new ConcurrentDictionary<int,int>();
var dictionaryTwo = new Dictionary<int,int>();

DoStuffToDictionary(dictionaryOne);
DoStuffToDictionary(dictionaryTwo);

void DoStuffToDictionary(IDictionary<int,int> items) {
   // Manipulate dictionary    

   if (items[0] == -1) {
     items[0] = 0; // Dumb example, but are indexers like this OK?
  }
}
Run Code Online (Sandbox Code Playgroud)

Gro*_*roo 7

这段代码有几个问题:

  1. IDictionary接口可以通过任何类型的字典实现
    您的示例当然不是线程安全的,因为您正在使用IDictionary<int,int>接口,这不保证任何线程安全.甚至你的代码都将a Dictionary和a ConcurrentDictionary传递给方法.

  2. 事务需要是原子的才能使它们成为线程安全
    即使字典实现保证是线程安全的,你的代码也不会是因为你没有在两次调用之间锁定对字典的访问:

    if (items[0] == -1) {
        // <-- another thread can access items in this moment
        items[0] = 0;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 返回LINQ查询永远不是线程安全的
    如果您使用LINQ返回IEnumerableIQueriable从您的方法返回,那么锁几乎没有效果,除非您使用该ToList()方法立即计算表达式并缓存结果.这是因为LINQ仅"准备"查询以执行.如果IEnumerable从方法返回a,则方法结束后(因此,在锁定之外)将访问实际字典.

这段代码最大的问题在于你传递的是IDictionary实例,这意味着代码的其他部分可以直接访问它,并且必须非常小心地锁定同一个锁对象实例.这是痛苦的,容易错误地正确实施,容易意外破坏,并且难以察觉(竞争条件可能在极少数情况下出现症状).

您可以做几件事来改进代码:

  1. 不要传递IDictionary,而是自己的接口(首选)
    使字典成为实现一些自定义接口的类的私有成员,抽象所有操作,并使用锁来确保线程安全(或使用ConcurrentDictionary引擎盖) .这样,您可以确保使用相同的锁实例锁定所有调用.

  2. 不要使用界面,而是始终通过ConcurrentDictionary
    这将是线程安全的,只要你使用特定的原子其中一个方法ConcurrentDictionary提供了(GetOrAdd,AddOrUpdate,等).使用像您在示例中所做的简单访问方法将不是线程安全的,这意味着您仍然需要小心它.另外的缺点是,如果您需要,您将无法抽象功能(无法包装/代理字典,您将无法使用其他字典实现).

  3. 传递IDictionary,并锁定字典本身(根本不推荐).
    这是一个丑陋的黑客,不幸的是,它经常被使用.这意味着您需要在访问此词典的应用程序的每个部分执行此操作,并在此过程中进一步注意锁定多个操作.