¿No hay ConcurrentList<T> en .Net 4.0?
Me emocionó ver el nuevo System.Collections.Concurrent
espacio de nombres en .Net 4.0, ¡bastante agradable! He visto ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
y BlockingCollection
.
Una cosa que parece faltar misteriosamente es un archivo ConcurrentList<T>
. ¿Tengo que escribirlo yo mismo (o sacarlo de la web :))?
¿Me estoy perdiendo algo obvio aquí?
Lo probé hace un tiempo (también: en GitHub ). Mi implementación tuvo algunos problemas, de los que no hablaré aquí. Déjame contarte, más importante aún, lo que aprendí.
En primer lugar, no hay forma de que obtenga una implementación completa IList<T>
sin bloqueo y segura para subprocesos. En particular, las inserciones y eliminaciones aleatorias no van a funcionar, a menos que también te olvides del acceso aleatorio O(1) (es decir, a menos que hagas "trampa" y simplemente uses algún tipo de lista enlazada y dejes que la indexación sea una mierda).
Lo que pensé que podría valer la pena fue un subconjunto limitado y seguro para subprocesos de IList<T>
: en particular, uno que permitiría Add
y proporcionaría acceso aleatorio de solo lectura por índice (pero no Insert
, RemoveAt
, etc., y tampoco acceso de escritura aleatoria ).
Este fue el objetivo de mi ConcurrentList<T>
implementación . Pero cuando probé su rendimiento en escenarios multiproceso, descubrí que simplemente sincronizar adiciones a a List<T>
era más rápido . Básicamente, agregar a a List<T>
ya es increíblemente rápido; la complejidad de los pasos computacionales involucrados es minúscula (incrementar un índice y asignarlo a un elemento en una matriz; eso es realmente todo ). Necesitaría un montón de escrituras simultáneas para ver algún tipo de contención de bloqueo en esto; e incluso entonces, el rendimiento promedio de cada escritura aún superaría a la implementación más costosa, aunque sin bloqueo, en ConcurrentList<T>
.
En el caso relativamente raro de que la matriz interna de la lista necesite cambiar de tamaño, usted paga un pequeño costo. Finalmente, llegué a la conclusión de que este era el único escenario de nicho en el que un tipo de colección de solo agregar ConcurrentList<T>
tendría sentido: cuando desea garantizar una baja sobrecarga al agregar un elemento en cada llamada (es decir, a diferencia de un objetivo de rendimiento amortizado).
Simplemente no es una clase tan útil como se podría pensar.
¿Para qué usarías una ConcurrentList?
El concepto de un contenedor de acceso aleatorio en un mundo de subprocesos no es tan útil como parece. La declaración
if (i < MyConcurrentList.Count)
x = MyConcurrentList[i];
en su conjunto todavía no sería seguro para subprocesos.
En lugar de crear una ConcurrentList, intente crear soluciones con lo que hay allí. Las clases más comunes son ConcurrentBag y especialmente BlockingCollection.
Con el debido respeto a las excelentes respuestas proporcionadas ya, hay ocasiones en las que simplemente quiero una IList segura para subprocesos. Nada avanzado ni sofisticado. El rendimiento es importante en muchos casos, pero a veces eso simplemente no es una preocupación. Sí, siempre habrá desafíos sin métodos como "TryGetValue", etc., pero en la mayoría de los casos solo quiero algo que pueda enumerar sin tener que preocuparme por bloquear todo. Y sí, probablemente alguien pueda encontrar algún "error" en mi implementación que podría llevar a un punto muerto o algo así (supongo), pero seamos honestos: cuando se trata de subprocesos múltiples, si no escribes tu código correctamente, se está estancando de todos modos. Con eso en mente, decidí realizar una implementación simple de ConcurrentList que satisfaga estas necesidades básicas.
Y por si sirve de algo: hice una prueba básica agregando 10.000.000 de elementos a la Lista normal y a la Lista Concurrente y los resultados fueron:
Lista terminada en: 7793 milisegundos. Concurrente finalizado en: 8064 milisegundos.
public class ConcurrentList<T> : IList<T>, IDisposable
{
#region Fields
private readonly List<T> _list;
private readonly ReaderWriterLockSlim _lock;
#endregion
#region Constructors
public ConcurrentList()
{
this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
this._list = new List<T>();
}
public ConcurrentList(int capacity)
{
this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
this._list = new List<T>(capacity);
}
public ConcurrentList(IEnumerable<T> items)
{
this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
this._list = new List<T>(items);
}
#endregion
#region Methods
public void Add(T item)
{
try
{
this._lock.EnterWriteLock();
this._list.Add(item);
}
finally
{
this._lock.ExitWriteLock();
}
}
public void Insert(int index, T item)
{
try
{
this._lock.EnterWriteLock();
this._list.Insert(index, item);
}
finally
{
this._lock.ExitWriteLock();
}
}
public bool Remove(T item)
{
try
{
this._lock.EnterWriteLock();
return this._list.Remove(item);
}
finally
{
this._lock.ExitWriteLock();
}
}
public void RemoveAt(int index)
{
try
{
this._lock.EnterWriteLock();
this._list.RemoveAt(index);
}
finally
{
this._lock.ExitWriteLock();
}
}
public int IndexOf(T item)
{
try
{
this._lock.EnterReadLock();
return this._list.IndexOf(item);
}
finally
{
this._lock.ExitReadLock();
}
}
public void Clear()
{
try
{
this._lock.EnterWriteLock();
this._list.Clear();
}
finally
{
this._lock.ExitWriteLock();
}
}
public bool Contains(T item)
{
try
{
this._lock.EnterReadLock();
return this._list.Contains(item);
}
finally
{
this._lock.ExitReadLock();
}
}
public void CopyTo(T[] array, int arrayIndex)
{
try
{
this._lock.EnterReadLock();
this._list.CopyTo(array, arrayIndex);
}
finally
{
this._lock.ExitReadLock();
}
}
public IEnumerator<T> GetEnumerator()
{
return new ConcurrentEnumerator<T>(this._list, this._lock);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new ConcurrentEnumerator<T>(this._list, this._lock);
}
~ConcurrentList()
{
this.Dispose(false);
}
public void Dispose()
{
this.Dispose(true);
}
private void Dispose(bool disposing)
{
if (disposing)
GC.SuppressFinalize(this);
this._lock.Dispose();
}
#endregion
#region Properties
public T this[int index]
{
get
{
try
{
this._lock.EnterReadLock();
return this._list[index];
}
finally
{
this._lock.ExitReadLock();
}
}
set
{
try
{
this._lock.EnterWriteLock();
this._list[index] = value;
}
finally
{
this._lock.ExitWriteLock();
}
}
}
public int Count
{
get
{
try
{
this._lock.EnterReadLock();
return this._list.Count;
}
finally
{
this._lock.ExitReadLock();
}
}
}
public bool IsReadOnly
{
get { return false; }
}
#endregion
}
public class ConcurrentEnumerator<T> : IEnumerator<T>
{
#region Fields
private readonly IEnumerator<T> _inner;
private readonly ReaderWriterLockSlim _lock;
#endregion
#region Constructor
public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
{
this._lock = @lock;
this._lock.EnterReadLock();
this._inner = inner.GetEnumerator();
}
#endregion
#region Methods
public bool MoveNext()
{
return _inner.MoveNext();
}
public void Reset()
{
_inner.Reset();
}
public void Dispose()
{
this._lock.ExitReadLock();
}
#endregion
#region Properties
public T Current
{
get { return _inner.Current; }
}
object IEnumerator.Current
{
get { return _inner.Current; }
}
#endregion
}
La razón por la que no existe ConcurrentList es porque fundamentalmente no se puede escribir. La razón es que varias operaciones importantes en IList dependen de índices y eso simplemente no funcionará. Por ejemplo:
int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");
El efecto que busca el autor es insertar "perro" antes de "gato", pero en un entorno multiproceso, cualquier cosa puede pasarle a la lista entre esas dos líneas de código. Por ejemplo, otro hilo podría funcionar list.RemoveAt(0)
, desplazando toda la lista hacia la izquierda, pero lo más importante es que catIndex no cambiará. El impacto aquí es que la Insert
operación en realidad pondrá al "perro" después del gato, no antes que él.
Las diversas implementaciones que ve como "respuestas" a esta pregunta tienen buenas intenciones, pero como muestra lo anterior, no ofrecen resultados confiables. Si realmente desea una semántica similar a una lista en un entorno multiproceso, no puede lograrlo colocando bloqueos dentro de los métodos de implementación de la lista. Debe asegurarse de que cualquier índice que utilice viva completamente dentro del contexto del bloqueo. El resultado es que puede usar una Lista en un entorno multiproceso con el bloqueo correcto, pero no se puede hacer que la lista en sí exista en ese mundo.
Si cree que necesita una lista simultánea, en realidad solo hay dos posibilidades:
- Lo que realmente necesitas es un ConcurrentBag
- Necesita crear su propia colección, tal vez implementada con una Lista y su propio control de concurrencia.
Si tiene un ConcurrentBag y está en una posición en la que necesita pasarlo como IList, entonces tiene un problema, porque el método al que está llamando ha especificado que podrían intentar hacer algo como lo hice anteriormente con cat & perro. En la mayoría de los mundos, lo que eso significa es que el método al que estás llamando simplemente no está diseñado para funcionar en un entorno de subprocesos múltiples. Eso significa que o lo refactorizas para que así sea o, si no puedes, tendrás que manejarlo con mucho cuidado. Es casi seguro que se le pedirá que cree su propia colección con sus propios candados y llame al método infractor dentro de un candado.