C# 中 ConcurrentDictionary 必定线程安全吗?

根据 .NET 官方文档的定义:ConcurrentDictionary<TKey,TValue> Class 表示可由多个线程同时访问的线程安全的键/值对集合。这也是咱们在并发任务中比较经常使用的一个类型,但它真的是绝对线程安全的吗?api

仔细阅读官方文档,咱们会发如今文档的底部线程安全性小节里这样描述:数组

ConcurrentDictionary<TKey,TValue> 的全部公共和受保护的成员都是线程安全的,可从多个线程并发使用。可是,经过一个由 ConcurrentDictionary<TKey,TValue> 实现的接口的成员(包括扩展方法)访问时,不保证其线程安全性,而且可能须要由调用方进行同步。安全

也就是说,调用 ConcurrentDictionary 自己的方法和属性能够保证都是线程安全的。可是因为 ConcurrentDictionary 实现了一些接口(例如 ICollection、IEnumerable 和 IDictionary 等),使用这些接口的成员(或者这些接口的扩展方法)不能保证其线程安全性。System.Linq.Enumerable.ToList 方法就是其中的一个例子,该方法是 IEnumerable 的一个扩展方法,在 ConcurrentDictionary 实例上使用该方法,当它被其它线程改变时可能抛出 System.ArgumentException 异常。下面是一个简单的示例:并发

static void Main(string[] args)
{
    var cd = new ConcurrentDictionary<int, int>();
    Task.Run(() =>
    {
        var random = new Random();
        while (true)
        {
            var value = random.Next(10000);
            cd.AddOrUpdate(value, value, (key, oldValue) => value);
        }
    });

    while (true)
    {
        cd.ToList(); //调用 System.Linq.Enumerable.ToList,抛出 System.ArgumentException 异常
    }
}

System.Linq.Enumerable.ToList 扩展方法:dom

System.Linq.Enumerable.ToList

发生异常是由于扩展方法 ToList 中调用了 List 的构造函数,该构造函数接收一个 IEnumerable<T> 类型的参数,且该构造函数中有一个对 ICollection<T> 的优化(由 ConcurrentDictionary 实现的)。函数

System.Collections.Generic.List<T> 构造函数:优化

System.Collections.Generic.List

List 的构造函数中,首先经过调用 Count 获取字典的大小,而后以该大小初始化数组,最后调用 CopyTo 将全部 KeyValuePair 项从字典复制到该数组。由于字典是能够由多个线程改变的,在调用 Count 后且调用 CopyTo 前,字典的大小能够增长或者减小。当 ConcurrentDictionary 试图访问数组超出其边界时,将引起 ArgumentException 异常。线程

ConcurrentDictionary<TKey,TValue> 中实现的 ICollection.CopyTo 方法:
ConcurrentDictionary-CopyTo-ArgumentExceptioncode


若是您只须要一个包含字典全部项的单独集合,能够经过调用 ConcurrentDictionary.ToArray 方法来避免此异常。它完成相似的操做,可是操做以前先获取了字典的全部内部锁,保证了线程安全性。blog

ConcurrentDictionary-ToArray

注意,不要将此方法与 System.Linq.Enumerable.ToArray 扩展方法混淆,调用 Enumerable.ToArrayEnumerable.ToList 同样,可能引起 System.ArgumentException 异常。

看下面的代码中:

static void Main(string[] args)
{
    var cd = new ConcurrentDictionary<int, int>();
    Task.Run(() =>
    {
        var random = new Random();
        while (true)
        {
            var value = random.Next(10000);
            cd.AddOrUpdate(value, value, (key, oldValue) => value);
        }
    });

    while (true)
    {
        cd.ToArray(); //ConcurrentDictionary.ToArray, OK.
    }
}

此时调用 ConcurrentDictionary.ToArray,而不是调用 Enumerable.ToArray,由于后者是一个扩展方法,前者重载解析的优先级高于后者。因此这段代码不会抛出异常。

可是,若是经过字典实现的接口(继承自 IEnumerable)使用字典,将会调用 Enumerable.ToArray 方法并抛出异常。例如,下面的代码显式地将 ConcurrentDictionary 实例分配给一个 IDictionary 变量:

static void Main(string[] args)
{
    System.Collections.Generic.IDictionary<int, int> cd = new ConcurrentDictionary<int, int>();
    Task.Run(() =>
    {
        var random = new Random();
        while (true)
        {
            var value = random.Next(10000);
            cd[value] = value;
        }
    });

    while (true)
    {
        cd.ToArray(); //调用 System.Linq.Enumerable.ToArray,抛出 System.ArgumentException 异常
    }
}

此时调用 Enumerable.ToArray,就像调用 Enumerable.ToList 时同样,引起了 System.ArgumentException 异常。

总结

正如官方文档上所说的那样,ConcurrentDictionary 的全部公共和受保护的成员都是线程安全的,可从多个线程并发调用。可是,经过一个由 ConcurrentDictionary 实现的接口的成员(包括扩展方法)访问时,并非线程安全的,此时要特别注意。

若是须要一个包含字典全部项的单独集合,能够经过调用 ConcurrentDictionary.ToArray 方法获得,千万不能使用扩展方法 ToList,由于它不是线程安全的。


参考:


做者 : 技术译民
出品 : 技术译站

相关文章
相关标签/搜索