example concurrentbag concurrent c# .net concurrency locking concurrent-collections

concurrentbag - concurrentdictionary c# example



¿Por qué ConcurrentBag<T> es tan lento en.Net(4.0)? ¿Lo estoy haciendo mal? (11)

Antes de comenzar un proyecto, escribí una prueba simple para comparar el rendimiento de ConcurrentBag de (System.Collections.Concurrent) en relación con el bloqueo y las listas. Estoy extremadamente sorprendido de que ConcurrentBag sea 10 veces más lento que el bloqueo con una simple Lista. Por lo que entiendo, el ConcurrentBag funciona mejor cuando el lector y el escritor son el mismo hilo. Sin embargo, no había pensado que el rendimiento fuera mucho peor que los bloqueos tradicionales.

He realizado una prueba con dos paralelos para escribir y leer bucles de una lista / bolsa. Sin embargo, la escritura por sí misma muestra una gran diferencia:

private static void ConcurrentBagTest() { int collSize = 10000000; Stopwatch stopWatch = new Stopwatch(); ConcurrentBag<int> bag1 = new ConcurrentBag<int>(); stopWatch.Start(); Parallel.For(0, collSize, delegate(int i) { bag1.Add(i); }); stopWatch.Stop(); Console.WriteLine("Elapsed Time = {0}", stopWatch.Elapsed.TotalSeconds); }

En mi caja, esto tarda entre 3 y 4 segundos en ejecutarse, en comparación con los 0,5 a 0,9 segundos de este código:

private static void LockCollTest() { int collSize = 10000000; object list1_lock=new object(); List<int> lst1 = new List<int>(collSize); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); Parallel.For(0, collSize, delegate(int i) { lock(list1_lock) { lst1.Add(i); } }); stopWatch.Stop(); Console.WriteLine("Elapsed = {0}", stopWatch.Elapsed.TotalSeconds); }

Como mencioné, hacer lecturas y escrituras concurrentes no ayuda a la prueba de bolsa concurrente. ¿Estoy haciendo algo mal o esta estructura de datos es realmente lenta?

[EDITAR]: eliminé las tareas porque no las necesito aquí (el código completo tenía otra lectura de tarea)

[EDIT] Muchas gracias por las respuestas. Estoy teniendo dificultades para elegir "la respuesta correcta", ya que parece ser una mezcla de algunas respuestas.

Como señaló Michael Goldshteyn, la velocidad realmente depende de los datos. Darin señaló que debería haber más contención para que ConcurrentBag sea más rápido, y Parallel.For no necesariamente inicia el mismo número de subprocesos. Un punto para quitar es no hacer nada que no tenga que hacer dentro de una cerradura. En el caso anterior, no me veo haciendo nada dentro del bloqueo, excepto que puedo asignar el valor a una variable temporal.

Además, sixlettervariables señaló que la cantidad de subprocesos que se están ejecutando también puede afectar los resultados, aunque intenté ejecutar la prueba original en orden inverso y ConcurrentBag aún era más lento.

Hice algunas pruebas con 15 tareas iniciales y los resultados dependieron del tamaño de la colección, entre otras cosas. Sin embargo, ConcurrentBag se desempeñó casi tan bien o mejor que el bloqueo de una lista, hasta 1 millón de inserciones. Por encima de 1 millón, el bloqueo parecía ser mucho más rápido a veces, pero probablemente nunca tenga una estructura de datos más grande para mi proyecto. Aquí está el código que corrí:

int collSize = 1000000; object list1_lock=new object(); List<int> lst1 = new List<int>(); ConcurrentBag<int> concBag = new ConcurrentBag<int>(); int numTasks = 15; int i = 0; Stopwatch sWatch = new Stopwatch(); sWatch.Start(); //First, try locks Task.WaitAll(Enumerable.Range(1, numTasks) .Select(x => Task.Factory.StartNew(() => { for (i = 0; i < collSize / numTasks; i++) { lock (list1_lock) { lst1.Add(x); } } })).ToArray()); sWatch.Stop(); Console.WriteLine("lock test. Elapsed = {0}", sWatch.Elapsed.TotalSeconds); // now try concurrentBag sWatch.Restart(); Task.WaitAll(Enumerable.Range(1, numTasks). Select(x => Task.Factory.StartNew(() => { for (i = 0; i < collSize / numTasks; i++) { concBag.Add(x); } })).ToArray()); sWatch.Stop(); Console.WriteLine("Conc Bag test. Elapsed = {0}", sWatch.Elapsed.TotalSeconds);


Básicamente tienes muy pocas escrituras concurrentes y no hay contención ( Parallel.For no significa necesariamente muchos subprocesos). Intenta paralelizar las escrituras y observarás diferentes resultados:

class Program { private static object list1_lock = new object(); private const int collSize = 1000; static void Main() { ConcurrentBagTest(); LockCollTest(); } private static void ConcurrentBagTest() { var bag1 = new ConcurrentBag<int>(); var stopWatch = Stopwatch.StartNew(); Task.WaitAll(Enumerable.Range(1, collSize).Select(x => Task.Factory.StartNew(() => { Thread.Sleep(5); bag1.Add(x); })).ToArray()); stopWatch.Stop(); Console.WriteLine("Elapsed Time = {0}", stopWatch.Elapsed.TotalSeconds); } private static void LockCollTest() { var lst1 = new List<int>(collSize); var stopWatch = Stopwatch.StartNew(); Task.WaitAll(Enumerable.Range(1, collSize).Select(x => Task.Factory.StartNew(() => { lock (list1_lock) { Thread.Sleep(5); lst1.Add(x); } })).ToArray()); stopWatch.Stop(); Console.WriteLine("Elapsed = {0}", stopWatch.Elapsed.TotalSeconds); } }


Como dijo @ Darin-Dimitrov, sospecho que su Parallel.For no está generando el mismo número de subprocesos en cada uno de los dos resultados. Intente crear N hilos de forma manual para asegurarse de que realmente está viendo la contención de hilo en ambos casos.


Como respuesta general:

  • Las recopilaciones concurrentes que utilizan el bloqueo pueden ser muy rápidas si hay poca o ninguna contención para sus datos (es decir, bloqueos). Esto se debe al hecho de que tales clases de recopilación a menudo se crean utilizando primitivas de bloqueo muy económicas, especialmente cuando no están contentos.
  • Las colecciones sin bloqueo pueden ser más lentas, debido a los trucos utilizados para evitar los bloqueos y debido a otros cuellos de botella como el intercambio falso, la complejidad requerida para implementar su naturaleza sin bloqueo que conduce a la falta de memoria caché, etc.

Para resumir, la decisión de qué manera es más rápida depende en gran medida de las estructuras de datos empleadas y de la cantidad de contención de los bloqueos, entre otras cuestiones (por ejemplo, número de lectores frente a escritores en una disposición de tipo compartido / exclusivo).

Su ejemplo particular tiene un alto grado de contención, por lo que debo decir que estoy sorprendido por el comportamiento. Por otro lado, la cantidad de trabajo realizado mientras se mantiene el bloqueo es muy pequeña, por lo que tal vez haya poca contención para el bloqueo en sí, después de todo. También podría haber deficiencias en la implementación del manejo de concurrencia de ConcurrentBag, lo que hace que su ejemplo particular (con inserciones frecuentes y sin lecturas) sea un mal caso de uso.


Déjame preguntarte esto: ¿qué tan realista es que tendrías una aplicación que se agrega constantemente a una colección y nunca se lee de ella ? ¿Para qué sirve esta colección? (Esta no es una pregunta puramente retórica. Podría imaginar que existan usos en los que, por ejemplo, solo se lee de la colección en el cierre (para el registro) o cuando el usuario lo solicita. Sin embargo, creo que estos escenarios son bastante raros.)

Esto es lo que tu código está simulando. List<T>.Add llamadas List<T>.Add será muy rápida en todos los casos, salvo en el caso ocasional en el que la lista tenga que cambiar el tamaño de su matriz interna; pero esto es suavizado por todos los otros agregados que suceden muy rápidamente. Por lo tanto, es probable que no veas una cantidad significativa de contención en este contexto, especialmente las pruebas en una PC personal con, por ejemplo, incluso 8 núcleos (como dijiste en un comentario en algún lugar). Tal vez podría ver más contención en algo como una máquina de 24 núcleos, donde muchos núcleos pueden estar intentando agregar a la lista literalmente al mismo tiempo.

Es mucho más probable que la contención se arrastre cuando lees de tu colección, especialmente. en bucles foreach (o consultas LINQ que equivalen a bucles foreach debajo del capó) que requieren bloquear toda la operación para que no modifique su colección mientras realiza iteraciones sobre ella.

Si puede reproducir este escenario de manera realista, creo que verá ConcurrentBag<T> escala ConcurrentBag<T> mucho mejor de lo que muestra su prueba actual.

Actualización : Here hay un programa que escribí para comparar estas colecciones en el escenario que describí anteriormente (varios escritores, muchos lectores). Ejecutando 25 pruebas con un tamaño de colección de 10000 y 8 hilos de lectura, obtuve los siguientes resultados:

Took 529.0095 ms to add 10000 elements to a List<double> with 8 reader threads. Took 39.5237 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads. Took 309.4475 ms to add 10000 elements to a List<double> with 8 reader threads. Took 81.1967 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads. Took 228.7669 ms to add 10000 elements to a List<double> with 8 reader threads. Took 164.8376 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads. [ ... ] Average list time: 176.072456 ms. Average bag time: 59.603656 ms.

Claramente depende de lo que estés haciendo con estas colecciones.


Debido a que el cuerpo del bucle es pequeño, puede intentar usar el método Crear de la clase Partitioner ...

que le permite proporcionar un bucle secuencial para el cuerpo del delegado, de modo que el delegado se invoca solo una vez por partición, en lugar de una vez por iteración

Cómo: Acelerar los cuerpos de pequeños bucles




Mirar el programa que utiliza el visualizador de contención de MS muestra que ConcurrentBag<T> tiene un costo mucho mayor asociado con la inserción paralela que el simple hecho de bloquear una List<T> . Una cosa que noté es que parece haber un costo asociado con la rotación de los 6 hilos (usados ​​en mi máquina) para comenzar la primera ejecución de la ConcurrentBag<T> (ejecución en frío). Luego se utilizan 5 o 6 hilos con el código de la List<T> , que es más rápido (ejecución en caliente). Agregar otra ejecución de ConcurrentBag<T> después de la lista muestra que toma menos tiempo que la primera (ejecución en caliente).

Por lo que veo en la discusión, se dedica mucho tiempo a la implementación de la memoria ConcurrentBag<T> . Eliminar la asignación explícita de tamaño del código de List<T> ralentiza, pero no lo suficiente como para marcar la diferencia.

EDIT: parece ser que ConcurrentBag<T> mantiene internamente una lista por Thread.CurrentThread , se bloquea 2-4 veces dependiendo de si se está ejecutando en un nuevo hilo, y realiza al menos un Interlocked.Exchange . Como se señaló en MSDN: "optimizado para escenarios donde el mismo subproceso producirá y consumirá datos almacenados en la bolsa". Esta es la explicación más probable para su disminución de rendimiento en comparación con una lista en bruto.


Parece que ConcurrentBag es más lento que las otras colecciones concurrentes.

Creo que es un problema de implementación. ANTS Profiler muestra que se ha atascado en un par de lugares, incluida una copia de matriz.

Usar el diccionario concurrente es miles de veces más rápido.



Sería interesante ver la escala entre los dos.

Dos preguntas

1) qué tan rápido es el bolso frente a la lista para leer, recuerde poner un candado en la lista

2) ¿Qué tan rápido es el bolso frente a la lista para leer mientras otro hilo está escribiendo?