zoukankan      html  css  js  c++  java
  • 关于ConcurrentDictionary的线程安全

    ConcurrentDictionary是.net BCL的一个线程安全的字典类,由于其方法的线程安全性,使用时无需手动加锁,被广泛应用于多线程编程中。然而,有的时候他们并不是如我们预期的那样工作。

    拿它的一个GetOrAdd方法为例, 它的定义如下:

    public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);

    这是一个非常常用的方法,MSDN对它的描述为: 需要检索指定键的现有值,如果此键不存在,则需要指定一个键/值对。其行为模式是:

    • 第一次调用的时候会调用valueFactory创建值并返回
    • 后续调用的线程会直接返回字典中的检索值,valueFactory不会执行。

    也就是说valueFactory只会在第一次调用的时候执行。由于微软在MSDN中说明这个函数是线程安全的,我一直以为其在并发执行的时候行为也是一样的,认为valueFactory只会执行一次。并且它也运行结果也一直如我所预期,然而今天定位一个问题的时候,通过日志发现其valueFactory是会执行多次的。

    为了简单的展示这个问题,我这里写了一段简单的代码。 

    var dic = new ConcurrentDictionary<int, int>();
     
    for (int i = 0; i < 6; i++)
    {
        runInNewThread(i);
    }
     
    void runInNewThread(int i)
    {
        var thread = new Thread(para => dic.GetOrAdd(1, _ => getNum((int)para)));
        thread.Start(i);
    }
     
    int getNum(int i)
    {
        Console.WriteLine($"Factory invoke. got {i}");
        return i;
    }

    执行这段代码,结果如下:

    Factory invoke. got 1
    Factory invoke. got 4
    Factory invoke. got 2
    Factory invoke. got 0
    Factory invoke. got 3
    Factory invoke. got 5

    也就是说,其valueFactory函数getNum是执行了6次的,并不是和我预期的结果一样的。便回头翻了下MSDN,发现MSDN在文章如何:在 ConcurrentDictionary 中添加和移除项中描述了这个现象。

    简单的讲,微软设计这个函数时,将其设计成了线程安全的,但不是原子的。也就是说,微软的这个函数实现的方式是

    lock (getOperation)
    {
        get();
    }
     
    lock (addOperation)
    {
        create_add();
    }

    而我认为它的执行方式是,

    lock (operation)
    {
        get();
        create_add();
    }

    因此会出现我预期外的valueFactory函数执行多次的情况。微软MSDN中描述了一种更严重的情况:

    1. threadA 调用 GetOrAdd,未找到项,通过调用 valueFactory 委托创建要添加的新项。
    2. threadB 并发调用 GetOrAdd,其 valueFactory 委托受到调用,并且它在 threadA 之前到达内部锁,并将其新键值对添加到词典中。
    3. threadA 的用户委托完成,此线程到达锁位置,但现在发现已有项存在
    4. threadA 执行"Get",返回之前由 threadB 添加的数据。

    因此,无法保证 GetOrAdd 返回的数据与线程的 valueFactory 创建的数据相同。 调用 AddOrUpdate 时可能发生相似的事件序列。

    这个问题是非常隐蔽的,这个行为大部分的时候并不会造成问题,因为

    1. GetOrAdd同时执行的几率较小,valueFactory不会执行多遍
    2. 大部分的时候valueFactory是线程安全的,同时执行了多遍也看不出来

    网上也有人讨论了这个问题:

    对于valueFactory只允许执行一遍的场景,这两篇文章中也提到了同样的解决方法,那就是使用Lazy<Value>,相当于需要两次才执行实际的valueFactory函数。

    这种方式下,第一次valueFactory虽然会执行多遍,但没有执行实际的创建操作,而在使用的时候Lazy<Value>使用的时候Lazy的原子性保证第二次valueFactory创建操作只会执行一次。

    当然,也有更简单粗暴的做法,那就是对GetOrAdd和AddOrUpdate加锁,但那样的需要在所有调用的地方都加锁,实际实行起来很容易漏。

  • 相关阅读:
    互联网某些方面代替了朋友的作用
    穷人
    血脉之力
    鹤立鸡群
    如果有了一个进化的机会,你会选择放弃人类这个身份么?
    怎么样的制度才算是好制度
    /etc/fstab 参数详解及如何设置开机自动挂载
    Linux 查看系统硬件信息(实例详解)
    Linux下添加新硬盘,分区及挂载
    Quartz.NET
  • 原文地址:https://www.cnblogs.com/TianFang/p/9483977.html
Copyright © 2011-2022 走看看