.NET ConcurrentDictionary.ToArray() ArgumentException

Иногда я получаю следующую ошибку при вызове ConcurrentDictionary.ToArray. Ошибка ниже:

System.ArgumentException: индекс равен или больше длины массива, или количество элементов в словаре больше, чем доступное пространство от индекса до конца целевого массива. в массиве System.Collections.Concurrent.ConcurrentDictionary2.System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>.CopyTo(KeyValuePair2[], индекс Int32) в источнике System.Linq.Buffer1..ctor(IEnumerable1) в System.Linq.Enumerable.ToArray[TSource](IEnumerable1 source) at ...Cache.SlidingCache2.RemoveExcessAsync(состояние объекта) в ...\SlidingCache.cs :строка 141 в System.Threading.ExecutionContext.RunInternal(ExecutionContext ExecutionContext, обратный вызов ContextCallback, состояние объекта, логическое значение saveSyncCtx) в System.Threading.ExecutionContext.Run(ExecutionContext executeContext, обратный вызов ContextCallback, состояние объекта, логическое значение saveSyncCtx) в System.Threading. QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() в System.Threading.ThreadPoolWorkQueue.Dispatch()

Я заметил, что в многопоточных сценариях вы иногда получаете исключения при сортировке 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);
    }

Может ли кто-нибудь объяснить потенциальную причину вышеуказанного исключения и какие-либо обходные пути?

Спасибо.


person Kess    schedule 15.04.2015    source источник


Ответы (1)


Это связано с тем, что Enumerable.ToArray небезопасно использовать с параллельными коллекциями.

Вы должны объявить свою внутреннюю переменную типа ConcurrentDictionary, а не IDictionary, так как это будет использовать реализацию ToArray, реализованную самим словарем, вместо того, чтобы полагаться на метод расширения:

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

В частности, 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);
        }
    }

Как видите, он использует свойство словаря 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);
    }
}
person Lasse V. Karlsen    schedule 15.04.2015
comment
Большое тебе спасибо. Это имеет большой смысл. - person Kess; 15.04.2015