.NET ConcurrentDictionary.ToArray()ArgumentException

Kes*_*ess 10 c# multithreading concurrentdictionary

当我调用ConcurrentDictionary.ToArray时,有时我会收到以下错误.错误如下:

System.ArgumentException:索引等于或大于数组的长度,或者字典中的元素数大于从索引到目标数组末尾的可用空间.at System.Collections.Concurrent.ConcurrentDictionary 2.System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>.CopyTo(KeyValuePair2 [] array,Int32 index)at System.Linq.Buffer 1..ctor(IEnumerable1 source)at System.Linq.Enumerable.ToArray [TSource](IEnumerable 1 source) at ...Cache.SlidingCache2.RemoveExcessAsync(Object state)in ...\SlidingCache.cs:System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext,ContextCallback callback,Object state,Boolean preserveSyncCtx)的第141行,位于System.Threading.ExecutionContext.Run(ExecutionContext executionContext,ContextCallback callback,Object state,Boolean preserveSyncCtx) System.Threading.ThreadPoolWorkQueue.Dispatch()中的.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()

我注意到在多线程场景中,有时在对ConcurrentDictionary进行排序时会出现异常.见堆栈溢出的问题在这里.所以我在排序之前开始使用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);
    }
Run Code Online (Sandbox Code Playgroud)

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

谢谢.

ang*_*son 15

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

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

private readonly IDictionary<TKey, CacheValue> _cache = new ConcurrentDictionary<TKey, CacheValue>();
Run Code Online (Sandbox Code Playgroud)

特别是,Enumerable.ToArray最终在Buffer内部使用类,这里是如何定义该类的构造函数(它的开头):

(来自Enumerable.cs - 参考源)

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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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

(来自ConcurrentDictionary.cs - 参考源)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)