Me emocionó ver el nuevo System.Collections.Concurrent
espacio de nombres en .Net 4.0, ¡bastante agradable! Yo he visto ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
y BlockingCollection
.
Una cosa que parece faltar misteriosamente es una ConcurrentList<T>
. ¿Tengo que escribir eso yo mismo (o sacarlo de la web :))?
¿Me estoy perdiendo algo obvio aquí?
Respuestas:
Lo intenté hace un tiempo (también: en GitHub ). Mi implementación tuvo algunos problemas, que no voy a abordar aquí. Déjame decirte, lo más importante, lo que aprendí.
En primer lugar, no hay forma de que pueda obtener una implementación completa de
IList<T>
eso es sin bloqueo y sin hilos. En particular, las inserciones y eliminaciones aleatorias son no funcionarán, a menos que también se olvide del acceso aleatorio O (1) (es decir, a menos que "engañe" y simplemente use algún tipo de lista vinculada y deje que la indexación sea un asco).Lo que pensé que podría valer la pena era un subconjunto limitado de flujos seguros
IList<T>
: en particular, que permitiría unaAdd
y proporcionar al azar de sólo lectura el acceso de índice (pero noInsert
,RemoveAt
, etc., y también hay aleatoria de escritura de acceso).Este fue el objetivo de mi
ConcurrentList<T>
implementación . Pero cuando probé su rendimiento en escenarios de subprocesos múltiples, descubrí que simplemente sincronizar las adiciones a aList<T>
era más rápido . Básicamente, agregar a un yaList<T>
es muy rápido; La complejidad de los pasos computacionales involucrados es minúscula (incrementar un índice y asignar a un elemento en una matriz; eso es realmente ). Necesitaría un montón de escrituras concurrentes para ver cualquier tipo de contención de bloqueo en esto; e incluso entonces, el rendimiento promedio de cada escritura aún superaría la implementación más costosa, aunque sin bloqueoConcurrentList<T>
.En el caso relativamente raro de que la matriz interna de la lista deba redimensionarse, usted paga un pequeño costo. Así que, en última instancia, llegué a la conclusión de que este era el único escenario de nicho en el que un
ConcurrentList<T>
tipo de colección de solo agregar tendría sentido: cuando desea garantizar una baja sobrecarga de agregar un elemento en cada llamada (por lo tanto, a diferencia de un objetivo de rendimiento amortizado).Simplemente no es una clase tan útil como parece.
fuente
List<T>
que usa la sincronización basada en monitor de old skool, haySynchronizedCollection<T>
BCL oculto: msdn.microsoft.com/en-us/library/ms668265.aspxConcurrentList
sería una victoria sería cuando no hay mucha actividad agregando a la lista, pero hay muchos lectores concurrentes. Se podría reducir la sobrecarga de los lectores a una sola barrera de memoria (y eliminar incluso eso si los lectores no estuvieran preocupados por datos ligeramente obsoletos).ConcurrentList<T>
de tal manera que se garantice a los lectores que vean un estado consistente sin necesidad de ningún bloqueo, con una sobrecarga adicional relativamente leve. Cuando la lista se expande, por ejemplo, de tamaño 32 a 64, mantenga la matriz de tamaño 32 y cree una nueva matriz de tamaño 64. Al agregar cada uno de los siguientes 32 elementos, colóquelo en la ranura 32-63 de la nueva matriz y copie un elemento antiguo de la matriz de tamaño 32 a la nueva. Hasta que se agregue el elemento 64, los lectores buscarán en la matriz de tamaño 32 los elementos 0-31 y en la matriz de tamaño 64 los elementos 32-63.¿Para qué usarías una ConcurrentList?
El concepto de un contenedor de acceso aleatorio en un mundo enhebrado no es tan útil como parece. La declaración
en su conjunto todavía no sería seguro para subprocesos.
En lugar de crear una lista concurrente, intente crear soluciones con lo que hay allí. Las clases más comunes son ConcurrentBag y especialmente BlockingCollection.
fuente
Monitor
que de todos modos ya puede hacerlo, no hay razón para una lista concurrente.Con el debido respeto a las excelentes respuestas proporcionadas ya, hay veces que simplemente quiero una IList segura para subprocesos. Nada avanzado o elegante. El rendimiento es importante en muchos casos, pero a veces eso 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í, alguien probablemente pueda encontrar algún "error" en mi implementación que pueda conducir a un punto muerto o algo así (supongo), pero seamos honestos: cuando se trata de subprocesos múltiples, si no escribe su código correctamente, se va a un punto muerto de todos modos. Con eso en mente, decidí hacer una implementación simple de ConcurrentList que satisfaga estas necesidades básicas.
Y para lo que vale: hice una prueba básica de agregar 10,000,000 artículos a List y ConcurrentList regulares y los resultados fueron:
Lista terminada en: 7793 milisegundos. Concurrente terminado en: 8064 milisegundos.
fuente
RemoveAt(int index)
nuncaInsert(int index, T item)
es seguro para subprocesos, solo es seguro para index == 0, el retorno deIndexOf()
está inmediatamente desactualizado, etc. Ni siquiera comience con elthis[int]
.ReaderWriterLockSlim
se puede hacer un punto muerto fácilmente mediante el usoEnterUpgradeableReadLock()
simultáneo. Sin embargo, no lo usa, no hace que la cerradura sea accesible al exterior y, por ejemplo, no llama a un método que ingresa un bloqueo de escritura mientras mantiene un bloqueo de lectura, por lo que usar su clase ya no hace puntos muertos. probable.var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";
. En general, cualquier combinación de lectura y escritura puede llevarlo a serios problemas cuando se realiza simultáneamente. Es por eso que las estructuras de datos concurrentes generalmente proporcionan métodos para aquellos, comoConcurrentDictionary
'sAddOrGet
etc. NB Su constante (y redundante porque los miembros ya están marcados como tales por el guión bajo) repetición dethis.
desorden.ConcurrentList
(como una matriz redimensionable, no una lista vinculada) no es fácil de escribir con operaciones sin bloqueo. Su API no se traduce bien a una versión "concurrente".fuente
La razón por la que no hay 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:
El efecto que busca el autor es insertar "perro" antes de "gato", pero en un entorno multiproceso, cualquier cosa puede pasar a la lista entre esas dos líneas de código. Por ejemplo, podría funcionar otro subproceso
list.RemoveAt(0)
, desplazando toda la lista a la izquierda, pero de manera crucial, catIndex no cambiará. El impacto aquí es que laInsert
operación realmente pondrá al "perro" detrás del gato, no antes.Las diversas implementaciones que ves que se ofrecen como "respuestas" a esta pregunta son bien intencionadas, pero como se muestra arriba, no ofrecen resultados confiables. Si realmente desea una semántica similar a una lista en un entorno multiproceso, no puede lograrlo colocando cerraduras dentro 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 exista en ese mundo.
Si cree que necesita una lista concurrente, en realidad solo hay dos posibilidades:
Si tiene un ConcurrentBag y está en una posición en la que necesita pasarlo como una 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 el gato & perro. En la mayoría de los mundos, lo que eso significa es que el método al que está llamando simplemente no está diseñado para funcionar en un entorno multiproceso. Eso significa que lo refactoriza para que esté o, si no puede, tendrá que manejarlo con mucho cuidado. Es casi seguro que se te pedirá que crees tu propia colección con sus propios bloqueos y que llames al método ofensivo dentro de un bloqueo.
fuente
En los casos en que las lecturas superan en número a las escrituras, o (aunque sean frecuentes) las escrituras no son concurrentes , una copia en escritura enfoque de puede ser apropiado.
La implementación que se muestra a continuación es
var snap = _list; snap[snap.Count - 1];
, nunca se lanzará (bueno, excepto por una lista vacía, por supuesto), y también obtendrá una enumeración segura de subprocesos con semántica de instantáneas gratis ... ¡cómo ME ENCANTA la inmutabilidad!Para que la copia en escritura funcione, debe mantener sus estructuras de datos efectivamente inmutables , es decir, a nadie se le permite cambiarlas después de ponerlas a disposición de otros hilos. Cuando quieres modificar, tú
Código
Uso
Si necesita más rendimiento, ayudará a desgenerizar el método, por ejemplo, crear un método para cada tipo de modificación (Agregar, Eliminar, ...) que desee y codificar los punteros de función
cloner
yop
.NB # 1 Es su responsabilidad asegurarse de que nadie modifique la estructura de datos (supuestamente) inmutable. No hay nada que podamos hacer en una implementación genérica para evitar eso, pero cuando se especialice
List<T>
, podría protegerse contra modificaciones utilizando List.AsReadOnly ()NB # 2 Tenga cuidado con los valores en la lista. El enfoque de copia en escritura anterior protege solo su pertenencia a la lista, pero si no pone cadenas, sino algunos otros objetos mutables, debe tener cuidado de la seguridad del hilo (por ejemplo, bloqueo). Pero eso es ortogonal a esta solución y, por ejemplo, el bloqueo de los valores mutables se puede usar fácilmente sin problemas. Solo necesitas estar consciente de ello.
NB # 3 Si su estructura de datos es enorme y la modifica con frecuencia, el enfoque de copiar todo en escritura podría ser prohibitivo tanto en términos de consumo de memoria como del costo de copia de la CPU involucrado. En ese caso, es posible que desee utilizar las colecciones inmutables de MS en su lugar.
fuente
System.Collections.Generic.List<t>
ya es seguro para múltiples lectores. Intentar hacer que sea seguro para múltiples escritores no tendría sentido. (Por razones que Henk y Stephen ya mencionaron)fuente
Algunas personas destacaron algunos puntos de bienes (y algunos de mis pensamientos):
Esa no es una respuesta. Estos son solo comentarios que realmente no se ajustan a un lugar específico.
... Mi conclusión, Microsoft tiene que hacer algunos cambios profundos en el "foreach" para que la colección MultiThreaded sea más fácil de usar. También tiene que seguir sus propias reglas de uso de IEnumerator. Hasta ese momento, podemos escribir una MultiThreadList fácilmente que usaría un iterador de bloqueo pero que no seguirá a "IList". En su lugar, tendrá que definir su propia interfaz "IListPersonnal" que podría fallar en "insertar", "eliminar" y el acceso aleatorio (indexador) sin excepción. Pero, ¿quién querrá usarlo si no es estándar?
fuente
ConcurrentOrderedBag<T>
que incluiría una implementación de solo lecturaIList<T>
, pero también ofrecería unint Add(T value)
método totalmente seguro para subprocesos . No veo por quéForEach
se necesitarían cambios. Aunque Microsoft no lo dice explícitamente, su práctica sugiere que es perfectamente aceptableIEnumerator<T>
enumerar los contenidos de la colección que existían cuando se creó; la excepción modificada por la colección solo se requiere si el enumerador no podría garantizar una operación sin fallas.GetEnumerator
método público dejar una colección bloqueada después de que regrese; Tales diseños pueden conducir fácilmente a un punto muerto. NoIEnumerable<T>
proporciona ninguna indicación de si se puede esperar que se complete una enumeración incluso si se modifica una colección; lo mejor que puede hacer es escribir sus propios métodos para que lo hagan, y tener métodos que aceptenIEnumerable<T>
documentar el hecho de que solo será seguro paraIEnumerable<T>
subprocesos si admite la enumeración segura para subprocesos.IEnumerable<T>
hubiera incluido un método de "Instantánea" con el tipo de retornoIEnumerable<T>
. Las colecciones inmutables podrían devolverse; una colección limitada si no otra cosa podría copiarse en unaList<T>
oT[]
y llamadaGetEnumerator
en eso. Se podrían implementar algunas colecciones ilimitadasSnapshot
, y aquellas que no podrían podrían lanzar una excepción sin tratar de llenar una lista con sus contenidos.Al ejecutar secuencialmente el código, las estructuras de datos utilizadas son diferentes del código de ejecución simultánea (bien escrito). La razón es que el código secuencial implica un orden implícito. Sin embargo, el código concurrente no implica ningún orden; ¡mejor aún implica la falta de un orden definido!
Debido a esto, las estructuras de datos con orden implícito (como List) no son muy útiles para resolver problemas concurrentes. Una lista implica orden, pero no define claramente cuál es ese orden. Debido a esto, el orden de ejecución del código que manipula la lista determinará (hasta cierto punto) el orden implícito de la lista, que está en conflicto directo con una solución concurrente eficiente.
¡Recuerde que la concurrencia es un problema de datos, no un problema de código! No puede implementar el código primero (o reescribir el código secuencial existente) y obtener una solución concurrente bien diseñada. Primero debe diseñar las estructuras de datos teniendo en cuenta que el ordenamiento implícito no existe en un sistema concurrente.
fuente
El enfoque de copiar y escribir sin cerradura funciona muy bien si no se trata de demasiados elementos. Aquí hay una clase que escribí:
ejemplo de uso: orders_BUY = CopyAndWriteList.Clear (orders_BUY);
fuente
Implementé uno similar al de Brian . El mío es diferente:
yield return
para producir un enumerador.DoSync
yGetSync
métodos que permiten interacciones secuenciales que requieren acceso exclusivo a la lista.El código :
fuente
try
bloqueRemove
o al indexador al mismo tiempo?IList
semántica en escenarios concurrentes es, en el mejor de los casos, limitada. Escribí este código probablemente antes de darme cuenta de eso. Mi experiencia es la misma que la del escritor de la respuesta aceptada: lo intenté con lo que sabía sobre sincronización e IList <T> y aprendí algo al hacerlo.