.NET - Bloqueo de diccionario versus ConcurrentDictionary

131

No pude encontrar suficiente información sobre los ConcurrentDictionarytipos, así que pensé en preguntar aquí.

Actualmente, utilizo a Dictionarypara mantener a todos los usuarios a los que acceden constantemente múltiples subprocesos (desde un grupo de subprocesos, por lo que no hay una cantidad exacta de subprocesos), y tiene acceso sincronizado.

Recientemente descubrí que había un conjunto de colecciones seguras para subprocesos en .NET 4.0, y parece ser muy agradable. Me preguntaba, cuál sería la opción 'más eficiente y más fácil de administrar', ya que tengo la opción entre tener un Dictionaryacceso normal con acceso sincronizado o tener uno ConcurrentDictionaryque ya sea seguro para subprocesos.

Referencia a .NET 4.0 ConcurrentDictionary

TheAJ
fuente
3
Es posible que desee ver este artículo del proyecto de código sobre el tema
nawfal

Respuestas:

146

Una colección segura para subprocesos frente a una colección no segura para subprocesos puede considerarse de una manera diferente.

Considere una tienda sin empleado, excepto al momento de pagar. Tienes muchos problemas si las personas no actúan de manera responsable. Por ejemplo, digamos que un cliente toma una lata de una lata piramidal mientras un empleado está construyendo la pirámide, todo el infierno se desataría. O, ¿qué pasa si dos clientes alcanzan el mismo artículo al mismo tiempo, quién gana? ¿Habrá una pelea? Esta es una colección no segura para subprocesos. Hay muchas maneras de evitar problemas, pero todas requieren algún tipo de bloqueo, o más bien acceso explícito de una forma u otra.

Por otro lado, considere una tienda con un empleado en un escritorio, y solo puede comprar a través de él. Te pones en la fila y le pides un artículo, él te lo devuelve y sales de la fila. Si necesita varios artículos, solo puede recoger tantos artículos en cada viaje de ida y vuelta como pueda recordar, pero debe tener cuidado para evitar acaparar al empleado, esto enojará a los otros clientes en la fila detrás de usted.

Ahora considera esto. En la tienda con un empleado, ¿qué pasa si llegas hasta el frente de la fila y le preguntas al empleado "¿Tienes papel higiénico?", Y él dice "Sí", y luego dices "Ok, yo" me pondré en contacto contigo cuando sepa cuánto necesito ", y para cuando vuelvas a estar al frente de la fila, la tienda, por supuesto, se puede agotar. Este escenario no se evita mediante una colección segura para subprocesos.

Una colección segura para subprocesos garantiza que sus estructuras de datos internas sean válidas en todo momento, incluso si se accede desde múltiples subprocesos.

Una colección que no sea segura para subprocesos no viene con tales garantías. Por ejemplo, si agrega algo a un árbol binario en un subproceso, mientras que otro subproceso está ocupado reequilibrando el árbol, no hay garantía de que el elemento se agregará, o incluso si el árbol sigue siendo válido después, podría estar corrupto más allá de la esperanza.

Sin embargo, una colección segura para subprocesos no garantiza que todas las operaciones secuenciales en el subproceso funcionen en la misma "instantánea" de su estructura de datos interna, lo que significa que si tiene un código como este:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

es posible que obtenga una NullReferenceException porque entre tree.County tree.First(), otro subproceso ha eliminado los nodos restantes en el árbol, lo que significa First()que volverá null.

Para este escenario, debe ver si la colección en cuestión tiene una forma segura de obtener lo que desea, tal vez necesite reescribir el código anterior o deba bloquear.

Lasse V. Karlsen
fuente
9
Cada vez que leo este artículo, aunque es cierto, que de alguna manera estamos yendo por el camino equivocado.
scope_creep
19
El principal problema en una aplicación habilitada para subprocesos son las estructuras de datos mutables. Si puede evitar eso, se ahorrará un montón de problemas.
Lasse V. Karlsen
89
Esta es una buena explicación de la seguridad de los subprocesos, sin embargo, no puedo evitar sentir que en realidad no abordó la pregunta del OP. Lo que preguntó el OP (y lo que posteriormente encontré con esta pregunta buscando) fue la diferencia entre usar un estándar Dictionaryy manejar el bloqueo usted mismo versus usar el ConcurrentDictionarytipo que está integrado en .NET 4+. En realidad estoy un poco desconcertado de que esto haya sido aceptado.
mclark1129
44
La seguridad de los hilos es más que simplemente usar la colección correcta. Usar la colección correcta es un comienzo, no es lo único con lo que tiene que lidiar. Supongo que eso es lo que el OP quería saber. No puedo adivinar por qué esto fue aceptado, ha pasado un tiempo desde que escribí esto.
Lasse V. Karlsen
2
Creo que lo que @ LasseV.Karlsen está tratando de decir es que ... la seguridad del hilo es fácil de lograr, pero debe proporcionar un nivel de concurrencia en la forma en que maneja esa seguridad. Pregúntese esto: "¿Puedo hacer más operaciones al mismo tiempo mientras mantengo el objeto seguro?" Considere este ejemplo: dos clientes desean devolver artículos diferentes. Cuando usted, como gerente, realiza el viaje para reabastecer su tienda, ¿no puede simplemente reabastecer ambos artículos en un solo viaje y aún manejar las solicitudes de ambos clientes, o va a hacer un viaje cada vez que un cliente devuelve un artículo?
Alexandru
68

Aún debe ser muy cuidadoso al usar colecciones seguras para subprocesos porque no puede ignorar todos los problemas de subprocesos. Cuando una colección se anuncia a sí misma como segura para subprocesos, generalmente significa que permanece en un estado consistente incluso cuando varios subprocesos están leyendo y escribiendo simultáneamente. Pero eso no significa que un solo hilo verá una secuencia "lógica" de resultados si llama a varios métodos.

Por ejemplo, si primero verifica si existe una clave y luego obtiene el valor que corresponde a la clave, es posible que esa clave ya no exista incluso con una versión ConcurrentDictionary (porque otro hilo podría haber eliminado la clave). Aún necesita usar el bloqueo en este caso (o mejor: combine las dos llamadas usando TryGetValue ).

Así que úselos, pero no piense que le da un pase libre para ignorar todos los problemas de concurrencia. Aún debes tener cuidado.

Mark Byers
fuente
44
¿Entonces básicamente estás diciendo que si no usas el objeto correctamente no funcionará?
ChaosPandion
3
Básicamente, necesitas usar TryAdd en lugar de los comunes Contiene -> Agregar.
ChaosPandion
3
@Bruno: No. Si no ha bloqueado, otro hilo puede haber eliminado la clave entre las dos llamadas.
Mark Byers
13
No quiero sonar corto aquí, pero TryGetValuesiempre ha sido parte Dictionaryy siempre ha sido el enfoque recomendado. Los métodos "concurrentes" importantes que ConcurrentDictionarypresenta son AddOrUpdatey GetOrAdd. Entonces, buena respuesta, pero podría haber escogido un mejor ejemplo.
Aaronaught
44
Correcto: a diferencia de la base de datos que tiene transacciones, la mayoría de las estructuras de búsqueda simultáneas en memoria omiten el concepto de aislamiento , solo garantizan que la estructura de datos interna sea correcta.
Chang
39

Internally ConcurrentDictionary utiliza un bloqueo separado para cada cubo de hash. Siempre que use solo Add / TryGetValue y métodos similares que funcionan en entradas individuales, el diccionario funcionará como una estructura de datos casi libre de bloqueos con el respectivo beneficio de rendimiento dulce. OTOH, los métodos de enumeración (incluida la propiedad Count) bloquean todos los depósitos a la vez y, por lo tanto, son peores que un diccionario sincronizado, en cuanto al rendimiento.

Yo diría que solo use ConcurrentDictionary.

Stefan Dragnev
fuente
15

Creo que el método ConcurrentDictionary.GetOrAdd es exactamente lo que necesitan la mayoría de los escenarios de subprocesos múltiples.

Konstantin
fuente
1
También usaría TryGet, de lo contrario malgastará el esfuerzo en inicializar el segundo parámetro en GetOrAdd cada vez que la clave ya exista.
Jorrit Schippers
77
GetOrAdd tiene la sobrecarga GetOrAdd (TKey, Func <TKey, TValue>) , por lo que el segundo parámetro se puede inicializar de forma diferida solo en caso de que la clave no exista en el diccionario. Además TryGetValue se utiliza para obtener datos, no modificarlos. Las llamadas posteriores a cada uno de estos métodos pueden causar que otro hilo haya agregado o eliminado la clave entre las dos llamadas.
sgnsajgon
Esta debería ser la respuesta marcada a la pregunta, a pesar de que la publicación de Lasse es excelente.
Spivonious
13

¿Has visto las extensiones reactivas para .Net 3.5sp1? Según Jon Skeet, han respaldado un paquete de extensiones paralelas y estructuras de datos concurrentes para .Net3.5 sp1.

Hay un conjunto de muestras para .Net 4 Beta 2, que describe con bastante buen detalle cómo usarlas las extensiones paralelas.

Acabo de pasar la última semana probando el ConcurrentDictionary usando 32 hilos para realizar E / S. Parece funcionar como se anuncia, lo que indicaría que se ha realizado una gran cantidad de pruebas.

Editar : .NET 4 ConcurrentDictionary y patrones.

Microsoft ha lanzado un pdf llamado Patterns of Paralell Programming. Realmente vale la pena descargarlo, ya que describe con detalles realmente agradables los patrones correctos para usar con las extensiones concurrentes de .Net 4 y los patrones anti para evitar. Aquí está.

alcance_creep
fuente
4

Básicamente quieres ir con el nuevo ConcurrentDictionary. Desde el primer momento, debe escribir menos código para que los programas sean seguros para subprocesos.

ChaosPandion
fuente
¿Hay algún problema si sigo adelante con Dictionery concurrente?
kbvishnu
1

El ConcurrentDictionaryes una gran opción si cumple todas sus necesidades de hilo de seguridad. Si no es así, en otras palabras, está haciendo algo un poco complejo, un Dictionary+ normal lockpuede ser una mejor opción. Por ejemplo, supongamos que está agregando algunas órdenes a un diccionario y desea mantener actualizado el monto total de las órdenes. Puede escribir código como este:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Este código no es seguro para subprocesos. Varios subprocesos que actualizan el _totalAmountcampo pueden dejarlo en un estado dañado. Por lo tanto, puede intentar protegerlo con lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Este código es "más seguro", pero aún no es seguro para subprocesos. No hay garantía de que _totalAmountsea ​​consistente con las entradas en el diccionario. Otro hilo puede intentar leer estos valores, para actualizar un elemento de la interfaz de usuario:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

Es totalAmountposible que no correspondan al recuento de pedidos en el diccionario. Las estadísticas mostradas pueden estar equivocadas. En este punto, se dará cuenta de que debe extender la lockprotección para incluir la actualización del diccionario:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Este código es perfectamente seguro, pero todos los beneficios de usar a ConcurrentDictionaryse han ido.

  1. El rendimiento ha empeorado en comparación con el uso normal Dictionary, ya que el bloqueo interno dentro del ConcurrentDictionarydispositivo ahora es derrochador y redundante.
  2. Debe revisar todo su código para usos desprotegidos de las variables compartidas.
  3. Estás atascado con el uso de una API incómoda ( TryAdd?, AddOrUpdate?).

Entonces, mi consejo es: comience con un Dictionary+ locky mantenga la opción de actualizar más tarde a a ConcurrentDictionarycomo una optimización del rendimiento, si esta opción es realmente viable. En muchos casos no lo será.

Theodor Zoulias
fuente
0

Hemos utilizado ConcurrentDictionary para la recopilación en caché, que se vuelve a llenar cada 1 hora y luego se lee por varios subprocesos de clientes, similar a la solución para ¿Es seguro este subproceso de ejemplo? pregunta.

Descubrimos que cambiarlo a  ReadOnlyDictionary  mejoraba el rendimiento general.

Michael Freidgeim
fuente
ReadOnlyDictionary es solo el contenedor alrededor de otro IDictionary. ¿Qué usas debajo del capó?
Lu55
@ Lu55, estábamos usando la biblioteca MS .Net y elegimos entre los diccionarios disponibles / aplicables. No estoy al tanto de la implementación interna de MS de ReadOnlyDictionary
Michael Freidgeim