paint-brush
Comprender las colecciones concurrentes en C#por@anatolyp
10,274 lecturas
10,274 lecturas

Comprender las colecciones concurrentes en C#

por Anatoly Pashmorga2021/12/15
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

System.NET's System.Collections.Concurrent` es un espacio de nombres para trabajar con un entorno de subprocesos múltiples. Proporciona la adición y eliminación simultánea de elementos de varios subprocesos con los métodos `Add` y `Take`. Su mejor opción es alejarse de la simultaneidad tanto como sea posible, pero cuando no es posible, las colecciones concurrentes pueden ser útiles, aunque de ninguna manera son una varita mágica.

Company Mentioned

Mention Thumbnail
featured image - Comprender las colecciones concurrentes en C#
Anatoly Pashmorga HackerNoon profile picture

Puede necesitarlos más a menudo cuando parece. Por ejemplo, cuando realiza un desarrollo web del lado del servidor, se encuentra en el contexto de subprocesos múltiples porque cada solicitud se ejecuta en un subproceso independiente y, si tiene un servicio único en su aplicación, debe asegurarse de que todo el código del servicio sea a salvo de amenazas. En el desarrollo de la interfaz de usuario (WPF, Xamarin, lo que sea), siempre tenemos tareas principales y en segundo plano, y si un usuario y un servicio en segundo plano pueden modificar una colección desde la interfaz de usuario, debe asegurarse de que su código sea seguro para subprocesos.

¿Por qué las colecciones estándar no son seguras para subprocesos?

Comencemos con un ejemplo simple.

 if(!dictionary.KeyExists(key)) { dictionary.Add(key, value); }

Y echemos un vistazo a lo que puede suceder en el escenario de dos hilos:

Al ejecutar este código en varios subprocesos, puede haber una posibilidad en ambos subprocesos if el caso pasa, pero solo un subproceso podrá modificar el diccionario y obtendrá ArgumentException (un elemento con la misma clave ya existe en el diccionario).

Para trabajar con colecciones en un entorno de subprocesos múltiples en .NET , tenemos un espacio de nombres System.Collections.Concurrent . Echemos un vistazo muy breve al respecto.

¿Qué tenemos en System.Collections.Concurrent Namespace?

  • ConcurrentDictionary : un diccionario seguro para subprocesos de uso general al que se puede acceder mediante varios subprocesos al mismo tiempo

  • ConcurrentStack : una colección de último en entrar, primero en salir (LIFO) segura para subprocesos

  • ConcurrentQueue : una colección FIFO (primero en entrar, primero en salir) segura para subprocesos

  • ConcurrentBag : una colección desordenada de objetos segura para subprocesos. Este tipo mantiene una colección separada para cada subproceso para agregar y obtener elementos para que tengan un mejor rendimiento cuando el productor y el consumidor residen en el mismo subproceso.

  • BlockingCollection : proporciona adición y eliminación simultáneas de elementos de varios subprocesos con los métodos Add y Take (con sobrecargas TryAdd y TryTake ). También tiene capacidades de delimitación y bloqueo, lo que significa que puede establecer la capacidad máxima de la colección, y los productores se bloquearán cuando se alcance una cantidad máxima de elementos para evitar un consumo excesivo de memoria.


BlockingCollection es un contenedor para ConcurrentStack , ConcurrentQueue , ConcurrentBag . De forma predeterminada, utiliza ConcurrentStack bajo el capó, pero puede proporcionar una colección más adecuada para su caso de uso durante la inicialización.


Todas estas colecciones ( BlockingCollection , ConcurrentStack , ConcurrentQueue , ConcurrentBag ) implementan la interfaz IProducerConsumerCollection , por lo que siempre intente usarla y podrá cambiar fácilmente entre diferentes tipos de colecciones.


También hay Partitioner , OrderablePartitioner , EnumerablePartitionerOptions , que utiliza Parallel.ForEach para la segmentación de colecciones.

Ahora profundicemos un poco más y veamos el principal beneficio que ofrecen las recopilaciones concurrentes.

Integridad del estado interno

Echemos un vistazo a otro ejemplo: el método Enqueue de la implementación de la cola genérica estándar en .NET

 // Adds item to the tail of the queue. public void Enqueue(T item) { if (_size == _array.Length) { Grow(_size + 1); } _array[_tail] = item; MoveNext(ref _tail); _size++; _version++; }

Queue <T> usa una matriz para almacenar elementos y cambia el tamaño de esta matriz cuando es necesario. Además, utiliza las propiedades _head y _tail para los índices desde los que quitar o poner en cola los elementos, respectivamente. Del código, vemos que Enqueue consta de varios pasos. Verificamos la longitud de la matriz y la redimensionamos si es necesario, luego almacenamos el elemento en la matriz y actualizamos las propiedades _tail y _size . Por así decirlo, no es una operación atómica .


Por ejemplo, el subproceso 1 asigna un valor a _array[_tail] y, mientras modifica la propiedad _tail , el subproceso 2 asigna otro valor al mismo índice _tail y terminamos con un estado inconsistente de nuestra colección.

A diferencia del estándar, las colecciones concurrentes garantizan la integridad de una colección en un entorno de subprocesos múltiples. Pero esto tiene un precio.


Las colecciones simultáneas tendrán menos rendimiento que las colecciones estándar en un entorno de un solo subproceso. Y el peor rendimiento que obtendrá luego de acceder a un estado agregado de una colección concurrente. El estado agregado es un valor que requiere acceso exclusivo a todos los elementos de la colección (por ejemplo, las propiedades .Count o .IsEmpty ). Las colecciones concurrentes usan diferentes técnicas para optimizar el bloqueo (bloqueos granulares, gestión de colecciones separadas para diferentes subprocesos), pero para consultar el estado agregado, debe bloquear toda la colección, lo que podría bloquear múltiples subprocesos. Por lo tanto, evite consultar el estado agregado con demasiada frecuencia.

Condiciones de carrera

En ambos ejemplos, ya hemos visto que el resultado de una operación depende del orden en que los hilos hacen su trabajo. Este tipo de problemas se denominan condiciones de carrera . Y las colecciones concurrentes tienen una API específica para minimizar las condiciones de carrera. Echemos un vistazo a este ejemplo de un solo hilo:


 if (dictionary.ContainsKey(key)) { dictionary[key] += 1; } else { dictionary.Add(key, 1); }


Ya debe comprender que este código puede fallar en diferentes lugares si se ejecuta en un entorno de subprocesos múltiples. Para tratar estos casos, el diccionario concurrente tiene el método AddOrUpdate , que se puede usar así:


 var newValue = dictionary .AddOrUpdate(key, 1, (itemKey, itemValue) => itemValue + 1)


Aquí tenemos un delegado como tercer parámetro del método AddOrUpdate . Uno podría esperar que AddOrUpdate sea una operación atómica, y no tendremos ningún problema aquí. Aunque esta operación es realmente atómica, usa TryOrUpdate bajo el capó, y si este último no puede actualizar el valor actual (por ejemplo, el valor ya se actualizó desde otro hilo), entonces el delegado se ejecutará nuevamente con un nuevo itemValue . Por lo tanto, debemos recordar que el delegado se puede ejecutar varias veces y que no debe contener efectos secundarios ni una lógica que dependa de varias ejecuciones.


Para terminar, deberíamos decir que su mejor opción es alejarse de la concurrencia tanto como sea posible, pero cuando no es posible, las colecciones concurrentes pueden ser útiles, aunque de ninguna manera son una varita mágica.