List<T>.AddRange 在传递 ConcurrentDictionary 作为参数时抛出 ArgumentException

问题描述 投票:0回答:1

今天我怀疑

List<T>.AddRange
方法使用并发集合作为参数可能不安全,所以我做了一个实验来找出答案:

ConcurrentDictionary<int, int> dictionary = new();

for (int i = 1; i <= 50_000; i++)
    dictionary.TryAdd(i, default);

List<KeyValuePair<int, int>> list = new();

Thread thread = new(() =>
{
    for (int i = -1; i >= -50_000; i--)
        dictionary.TryAdd(i, default);
});
thread.Start();

list.AddRange(dictionary); // Throws

thread.Join();
Console.WriteLine($"dictionary.Count: {dictionary.Count:#,0}, list.Count: {list.Count:#,0}");

在线演示.

ConcurrentDictionary
用 50,000 个正键初始化。然后在另一个线程上添加 50,000 个额外的负键,同时使用
AddRange
方法将字典添加到列表中。我预计最终字典将有 100,000 个键,列表将包含 50,000 到 100,000 个项目。事实上我得到了一个
ArgumentException
:

Unhandled exception. System.ArgumentException: The index is equal to or greater than the length of the array, or the number of elements in the dictionary is greater than the available space from index to the end of the destination array.
   at System.Collections.Concurrent.ConcurrentDictionary`2.System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>.CopyTo(KeyValuePair`2[] array, Int32 index)
   at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
   at System.Collections.Generic.List`1.AddRange(IEnumerable`1 collection)
   at Program.Main()

我的问题是:为什么会发生这种情况,我该如何防止它发生?有什么方法可以确保

list.AddRange(dictionary);
行始终成功,不会抛出异常?

想象一下,这本字典可能是作为

IEnumerable<T>
提供给我的,而我不知道它的底层类型。在这种情况下也会引发相同的异常:

IEnumerable<KeyValuePair<int, int>> enumerable = dictionary;
list.AddRange(enumerable); // Throws

这种行为降低了我对使用

List<T>.AddRange
API 的信心。

上下文:this问题中提到了类似的症状,但没有提供最小且可重现的示例,因此我不确定情况是否相同。另一个相关的问题是 this,关于在 ToList

 上调用 LINQ 
ConcurrentDictionary<TKey, TValue>
。尽管如此,文档 警告 关于在并发集合上使用扩展方法,但我是没有看到任何关于使用并发集合与 
List<T>.AddRange
方法的警告。

c# list exception concurrency concurrentdictionary
1个回答
1
投票

发生的事情相当简单。

List<T>.AddRange
有一个检查,看看它传递的东西是否是
ICollection<T>
。如果是这样,它可以通过使用
ICollection<T>.Count
一次性为新范围分配足够的空间(而不是可能多次调整列表大小)和
ICollection<T>.CopyTo
一次性复制集合的元素而不是添加它们来进行优化一对一。

代码是这里

if (collection is ICollection<T> c)
{
    int count = c.Count;
    if (count > 0)
    {
        if (_items.Length - _size < count)
        {
            Grow(checked(_size + count));
        }

        c.CopyTo(_items, _size);
        _size += count;
        _version++;
    }
}

ConcurrentDictionare<TKey, TValue>
实现了
ICollection<KeyValuePair<TKey, TValue>>
,并且它的
Count
CopyTo
的实现本身是安全的,但它们之间没有固有的同步。

因此

List<T>.AddRange
向字典询问其大小,分配该数量的新元素,然后要求字典将自身复制到新分配的空间中。然而,字典到那时已经增长了,并抛出异常here:

int count = GetCountNoLocks();
if (array.Length - count < index)
{
    throw new ArgumentException(SR.ConcurrentDictionary_ArrayNotLargeEnough);
}

至于谁应该“怪罪”这里,我不确定。

List<T>
所做的优化在大多数情况下都是明智的,并且作为一个非线程安全集合,它并不试图成为线程安全的。
ConcurrentDictionary
正在尽可能地实现
ICollection<T>
,并且如果要求复制到的空间不够大,
ICollection<T>.CopyTo
会被 记录为抛出

© www.soinside.com 2019 - 2024. All rights reserved.