2015-04-15 16 views
8

A volte viene visualizzato l'errore qui sotto quando chiamo ConcurrentDictionary.ToArray. Errore sotto:.NET ConcurrentDictionary.ToArray() ArgumentException

System.ArgumentException: L'indice è uguale o maggiore della lunghezza della matrice, o il numero di elementi nel dizionario è maggiore dello spazio disponibile dall'indice alla fine della matrice di destinazione . a System.Collections.Concurrent.ConcurrentDictionary 2.System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>.CopyTo(KeyValuePair 2 [] matrice, indice Int32) a System.Linq.Buffer 1..ctor(IEnumerable 1 fonte) a System.Linq.Enumerable.ToArray [TSource] (IEnumerable 1 source) at ...Cache.SlidingCache 2.RemoveExcessAsync (Object state) in ... \ SlidingCache.cs: riga 141 in System.Threading.ExecutionContext.RunInternal (ExecutionContext executionContext, callback ContextCallback, stato dell'oggetto, preserveSyncCtx booleano) in System.Threading.ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) in System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() in System.Threading.ThreadPoolWorkQueue.Dispatch()

Ho notato che in scenari multithread si ottengono a volte eccezioni durante l'ordinamento di ConcurrentDictionary. Vedere la domanda di overflow dello stack here. Quindi ho iniziato a utilizzare ConcurrentDictionary.ToArray prima di ordinare invece. Sembra che ci siano ancora problemi durante la creazione della matrice.

Il dizionario concorrente viene utilizzato per una cache che conserva oggetti e svuota gli ultimi oggetti accessibili quando viene raggiunto il numero massimo di elementi impostato per la cache. La cache è accessibile da più thread e l'errore sopra riportato si verifica quando si tenta di rimuovere gli elementi precedenti in modo che i nuovi elementi possano essere aggiunti all'array. Si prega di vedere alcuni frammenti di codice qui sotto:

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); 
    } 

qualcuno può spiegare gentilmente il motivo potenziale per l'eccezione di cui sopra e le eventuali soluzioni alternative?

Grazie.

risposta

13

Questo perché Enumerable.ToArray non è sicuro da utilizzare con raccolte concorrenti.

Si dovrebbe dichiarare la variabile interna per essere di tipo ConcurrentDictionary e non IDictionary, in quanto ciò utilizzare il ToArray implementazione attuata dal dizionario sé, invece di basarsi sul metodo di estensione:

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

In particolare, Enumerable.ToArray finisce utilizzando una classe Buffer internamente, e qui è come il costruttore di questa classe è definita (l'inizio di essa):

(da 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); 
     } 
    } 

Come si può vedere, si utilizza la proprietà Count del dizionario, crea una matrice, quindi copia gli elementi alla matrice. Se il dizionario sottostante ha ottenuto almeno un altro elemento dopo aver letto Count ma prima del CopyTo hai il tuo problema.

È possibile contrasto che con l'attuazione del ToArray all'interno del dizionario stesso che utilizza il blocco:

(da 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); 
    } 
} 
+0

Grazie mille. Questo ha molto senso. – Kess

Problemi correlati