Sometimes I get the error below when I call ConcurrentDictionary.ToArray. Error Below:
System.ArgumentException: The index is equal to or greater than
That is because Enumerable.ToArray
is not safe to use with concurrent collections.
You should declare your internal variable to be of type ConcurrentDictionary
and not IDictionary
, as this would use the ToArray
implementation implemented by the dictionary itself, instead of relying on the extension method:
private readonly IDictionary<TKey, CacheValue> _cache = new ConcurrentDictionary<TKey, CacheValue>();
In particular, Enumerable.ToArray
ends up using a Buffer
class internally, and here is how the constructor of that class is defined (the start of it):
(from 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);
}
}
As you can see, it uses the Count
property of the dictionary, creates an array, then copies the elements to the array. If the underlying dictionary has gotten at least one other item after reading Count
but before CopyTo
you get your problem.
You can contrast that with the implementation of ToArray
inside the dictionary itself which uses locking:
(from 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);
}
}