.NET ConcurrentDictionary.ToArray() 参数异常

.NET ConcurrentDictionary.ToArray() ArgumentException

有时调用 ConcurrentDictionary.ToArray 时会出现以下错误。以下错误:

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.ConcurrentDictionary2.System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>.CopyTo(KeyValuePair2[] array, Int32 index) at System.Linq.Buffer1..ctor(IEnumerable1 source) at System.Linq.Enumerable.ToArray[TSource](IEnumerable1 source) at ...Cache.SlidingCache2.RemoveExcessAsync(Object state) in ...\SlidingCache.cs:line 141 at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() at System.Threading.ThreadPoolWorkQueue.Dispatch()

我注意到在多线程场景中,有时在对 ConcurrentDictionary 进行排序时会出现异常。请参阅堆栈溢出问题 here。所以我在排序之前开始使用 ConcurrentDictionary.ToArray 。创建数组时似乎仍然存在问题。

并发字典用于维护对象并在达到为缓存设置的最大元素数时刷新最后访问的对象的缓存。缓存由多个线程访问,并且在尝试删除旧元素以便可以将新元素添加到数组时发生上述错误。请查看下面的一些代码片段:

public class SlidingCache<TKey, TValue> : IDictionary<TKey, TValue>
{
    public int MinCount { get; private set; }
    public int MaxCount { get; private set; }
    private readonly IDictionary<TKey, CacheValue> _cache = new ConcurrentDictionary<TKey, CacheValue>();

    public SlidingCache(int minCount=75000, int maxCount=100000)
    {
        if (minCount <= 2)
            throw new ArgumentException("minCount");

        if (maxCount <= minCount)
            throw new ArgumentException("maxCount");

        MinCount = minCount;
        MaxCount = maxCount;
    }

    #region IDictionary<TKey, TValue>

    public int Count
    {
        get { return _cache.Count; }
    }

    public TValue this[TKey key]
    {
        get
        {
            return _cache[key].Value;
        }
        set
        {
            _cache[key]=new CacheValue(value);
            RemoveExcess();
        }
    }
...

    #endregion

    private void RemoveExcess()
    {
        if (this.Count <= this.MaxCount || Interlocked.Increment(ref _removingExcess) != 1)
            return;

        ThreadPool.QueueUserWorkItem(RemoveExcessAsync, null);
    }

    private int _removingExcess;
    private void RemoveExcessAsync(object state)
    {
        var remove = _cache.ToArray().OrderByDescending(i => i.Value.LastRequestTime).Take(MaxCount - MinCount);
        foreach (var pair in remove)
        {
            _cache.Remove(pair.Key);
        }

        Interlocked.Exchange(ref _removingExcess, 0);
    }

任何人都可以解释上述异常的潜在原因和任何解决方法吗?

谢谢。

那是因为 Enumerable.ToArray 与并发集合一起使用是不安全的。

您应该将内部变量声明为 ConcurrentDictionary 类型而不是 IDictionary,因为这将使用字典本身实现的 ToArray 实现,而不是依赖于扩展方法:

private readonly IDictionary<TKey, CacheValue> _cache = new ConcurrentDictionary<TKey, CacheValue>();

特别是,Enumerable.ToArray 最终在内部使用 Buffer class,这里是 class 的构造函数的定义方式(它的开始) :

(来自Enumerable.cs - reference source

internal Buffer(IEnumerable<TElement> source) {
    TElement[] items = null;
    int count = 0;
    ICollection<TElement> collection = source as ICollection<TElement>;
    if (collection != null) {
        count = collection.Count;
        if (count > 0) {
            items = new TElement[count];
            collection.CopyTo(items, 0);
        }
    }

如你所见,它使用字典的Count 属性,创建一个数组,然后将元素复制到数组中。如果基础字典在阅读 Count 之后但在 CopyTo 之前至少获得了一项其他项目,那么您就会遇到问题。

您可以将其与使用锁定的字典本身内部的 ToArray 实现进行对比:

(来自ConcurrentDictionary.cs - reference source

public KeyValuePair<TKey, TValue>[] ToArray()
{
    int locksAcquired = 0;
    try
    {
        AcquireAllLocks(ref locksAcquired);
        int count = 0;
        checked
        {
            for (int i = 0; i < m_tables.m_locks.Length; i++)
            {
                count += m_tables.m_countPerLock[i];
            }
        }

        KeyValuePair<TKey, TValue>[] array = new KeyValuePair<TKey, TValue>[count];

        CopyToPairs(array, 0);
        return array;
    }
    finally
    {
        ReleaseLocks(0, locksAcquired);
    }
}