c# multithreading locking volatile interlocked

c# - Volatile vs. Interlocked vs. Lock



multithreading locking (9)

Digamos que una clase tiene un campo de public int counter que se accede mediante varios subprocesos. Este int solo se incrementa o decrementa.

Para incrementar este campo, ¿qué enfoque se debe utilizar y por qué?

  • lock(this.locker) this.counter++; ,
  • Interlocked.Increment(ref this.counter); ,
  • Cambia el modificador de acceso de counter a public volatile .

Ahora que he descubierto que es volatile , he estado eliminando muchas declaraciones de lock y el uso de Interlocked . ¿Pero hay una razón para no hacer esto?


Lo peor (en realidad no funcionará)

Cambia el modificador de acceso de counter a public volatile

Como otras personas han mencionado, esto por sí solo no es realmente seguro. El punto de volatile es que varios subprocesos que se ejecutan en múltiples CPU pueden y almacenarán datos en caché y reordenarán las instrucciones.

Si no es volatile , y la CPU A incrementa un valor, entonces la CPU B puede no ver ese valor incrementado hasta algún tiempo después, lo que puede causar problemas.

Si es volatile , esto solo garantiza que las dos CPU vean los mismos datos al mismo tiempo. No les impide en absoluto intercalar sus operaciones de lectura y escritura, que es el problema que intenta evitar.

Segundo mejor:

lock(this.locker) this.counter++ ;

Es seguro hacerlo (siempre que recuerde lock cualquier otra parte que acceda a this.counter ). Evita que otros subprocesos ejecuten cualquier otro código que esté protegido por un locker . El uso de bloqueos también previene los problemas de reordenación de varias CPU que se mencionaron anteriormente, lo cual es excelente.

El problema es que el bloqueo es lento, y si reutilizas el locker en algún otro lugar que no esté realmente relacionado, puedes terminar bloqueando tus otros hilos sin ninguna razón.

Mejor

Interlocked.Increment(ref this.counter);

Esto es seguro, ya que efectivamente realiza la lectura, el incremento y la escritura en ''un hit'' que no se puede interrumpir. Debido a esto, no afectará a ningún otro código, y tampoco necesita recordar bloquearlo en otro lugar. También es muy rápido (como dice MSDN, en las CPU modernas, esto suele ser literalmente una única instrucción de CPU).

Sin embargo, no estoy del todo seguro de si logra sortear otras CPU reordenando las cosas, o si también necesita combinar volatilidad con el incremento.

Notas entrelazadas:

  1. LOS MÉTODOS INTERLOCADOS SON CONCURRENTEMENTE SEGUROS EN CUALQUIER NÚMERO DE CORE O CPU.
  2. Los métodos entrelazados aplican una valla completa en torno a las instrucciones que ejecutan, por lo que no se reordena.
  3. Los métodos interbloqueados no necesitan o incluso no admiten el acceso a un campo volátil , ya que la volatilidad se coloca en medio campo alrededor de las operaciones en un campo determinado y entrelazada usa el cercado completo.

Nota al pie: para qué es volátil realmente bueno.

Como volatile no previene este tipo de problemas de multihilo, ¿para qué sirve? Un buen ejemplo es decir que tiene dos subprocesos, uno que siempre escribe en una variable (por ejemplo, queueLength ), y otro que siempre se lee desde esa misma variable.

Si queueLength no es volátil, el subproceso A puede escribir cinco veces, pero el subproceso B puede ver esas escrituras como retrasadas (o incluso potencialmente en el orden incorrecto).

Una solución sería bloquear, pero también podría usar volatile en esta situación. Esto aseguraría que el subproceso B siempre verá lo más actualizado que el subproceso A ha escrito. Sin embargo, tenga en cuenta que esta lógica solo funciona si tiene escritores que nunca leen, y lectores que nunca escriben, y si lo que está escribiendo es un valor atómico. Tan pronto como realice una sola lectura-modificación-escritura, debe ir a las operaciones de bloqueo o usar un bloqueo.


" volatile " no reemplaza Interlocked.Increment ! Solo se asegura de que la variable no esté en caché, sino que se use directamente.

Incrementar una variable requiere en realidad tres operaciones:

  1. leer
  2. incremento
  3. escribir

Interlocked.Increment realiza las tres partes como una sola operación atómica.


Hice algunas pruebas para ver cómo funciona realmente la teoría: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Mi prueba se centró más en el Comparexchnage, pero el resultado para Incremento es similar. El enclavamiento no es necesario más rápido en un entorno multi-cpu. Aquí está el resultado de la prueba para Incremento en un servidor de 16 CPU de 2 años. Tenga en cuenta que la prueba también implica la lectura segura después del aumento, lo cual es típico en el mundo real.

D:/>InterlockVsMonitor.exe 16 Using 16 threads: InterlockAtomic.RunIncrement (ns): 8355 Average, 8302 Minimal, 8409 Maxmial MonitorVolatileAtomic.RunIncrement (ns): 7077 Average, 6843 Minimal, 7243 Maxmial D:/>InterlockVsMonitor.exe 4 Using 4 threads: InterlockAtomic.RunIncrement (ns): 4319 Average, 4319 Minimal, 4321 Maxmial MonitorVolatileAtomic.RunIncrement (ns): 933 Average, 802 Minimal, 1018 Maxmial


Las funciones enclavadas no se bloquean. Son atómicos, lo que significa que pueden completarse sin la posibilidad de un cambio de contexto durante el incremento. Así que no hay posibilidad de interbloqueo o espera.

Yo diría que siempre deberías preferirlo a un bloqueo e incremento.

La volatilidad es útil si necesita que las escrituras en un subproceso se lean en otro, y si desea que el optimizador no reordene las operaciones en una variable (porque están sucediendo cosas en otro subproceso que el optimizador no conoce). Es una elección ortogonal a cómo incrementas.

Este es un artículo realmente bueno si desea leer más sobre el código sin bloqueo y la forma correcta de escribirlo.

http://www.ddj.com/hpc-high-performance-computing/210604448


Lea el subproceso en la referencia de C # . Cubre los entresijos de su pregunta. Cada uno de los tres tiene diferentes efectos y efectos secundarios.


Lo que está buscando es un bloqueo o un incremento interbloqueado.

Definitivamente, lo volátil no es lo volátil, simplemente le dice al compilador que trate la variable como siempre cambiante, incluso si la ruta del código actual le permite al compilador optimizar una lectura de la memoria.

p.ej

while (m_Var) { }

Si m_Var se establece en falso en otro hilo pero no se declara como volátil, el compilador es libre de convertirlo en un bucle infinito (pero no significa que siempre lo hará) haciendo que se verifique contra un registro de CPU (por ejemplo, EAX porque era en qué m_Var se buscó desde el principio) en lugar de emitir otra lectura a la ubicación de la memoria de m_Var (esto puede estar almacenado en caché, no sabemos y no nos importa y ese es el punto de la coherencia de caché de x86 / x64). Todas las publicaciones anteriores de otros que mencionaron el reordenamiento de instrucciones simplemente muestran que no entienden las arquitecturas x86 / x64. Volatile no emite barreras de lectura / escritura como lo indican las publicaciones anteriores que dicen ''evita la reordenación''. De hecho, gracias al protocolo MESI, tenemos garantizado que el resultado que leemos es siempre el mismo en todas las CPU, independientemente de si los resultados reales se han retirado a la memoria física o simplemente residen en el caché de la CPU local. No voy a ir demasiado lejos en los detalles de esto, pero tenga la seguridad de que si esto sale mal, Intel / AMD probablemente emitirá un retiro del procesador. Esto también significa que no tenemos que preocuparnos por la ejecución fuera de orden, etc. Los resultados siempre están garantizados para retirarse en orden; de lo contrario, ¡estamos llenos!

Con el Incremento interbloqueado, el procesador debe apagarse, obtener el valor de la dirección indicada, luego incrementarlo y escribirlo, todo esto mientras se posee la propiedad exclusiva de toda la línea de caché (bloquear xadd) para asegurarse de que ningún otro procesador pueda modificar es valioso.

Con volatile, aún terminará con solo 1 instrucción (suponiendo que el JIT sea eficiente como debería) - inc dword ptr [m_Var]. Sin embargo, el procesador (cpuA) no solicita la propiedad exclusiva de la línea de caché mientras hace todo lo que hizo con la versión interbloqueada. Como puede imaginar, esto significa que otros procesadores podrían escribir un valor actualizado en m_Var después de que haya sido leído por cpuA. Entonces, en lugar de haber incrementado el valor dos veces, terminas con solo una vez.

Espero que esto aclare el problema.

Para obtener más información, consulte "Comprender el impacto de las técnicas de bloqueo bajo en aplicaciones de multiproceso": http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps ¿Qué provocó esta respuesta tan tardía? Todas las respuestas fueron tan descaradamente incorrectas (especialmente la marcada como respuesta) en su explicación que solo tuve que aclarar esto para cualquier persona que lea esto. encogerse de hombros

pps Supongo que el objetivo es x86 / x64 y no IA64 (tiene un modelo de memoria diferente). Tenga en cuenta que las especificaciones de ECMA de Microsoft están equivocadas, ya que especifica el modelo de memoria más débil en lugar del más fuerte (siempre es mejor especificar el modelo de memoria más fuerte para que sea coherente en todas las plataformas; de lo contrario, el código funcionaría 24-7 en x86 / x64 puede que no se ejecute en absoluto en IA64 aunque Intel ha implementado un modelo de memoria similarmente fuerte para IA64) - Microsoft admitió esto por sí mismo - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .



lock (...) funciona, pero puede bloquear un hilo y podría causar un interbloqueo si otro código utiliza los mismos bloqueos de una manera incompatible.

Enclavado. * Es la forma correcta de hacerlo ... mucho menos gastos generales, ya que las CPU modernas lo admiten como una primitiva.

La volatilidad por sí sola no es correcta. Un subproceso que intenta recuperar y luego volver a escribir un valor modificado aún podría entrar en conflicto con otro subproceso que haga lo mismo.


EDITAR: Como se señaló en los comentarios, en estos días estoy feliz de usar Interlocked para los casos de una sola variable donde, obviamente, está bien. Cuando se complique, volveré al bloqueo ...

El uso de volatile no ayudará cuando necesite incrementar, ya que la lectura y la escritura son instrucciones separadas. Otro hilo podría cambiar el valor después de haber leído, pero antes de volver a escribir.

Personalmente, casi siempre me limito a bloquear, es más fácil hacerlo bien de una manera que obviamente es la correcta que la volatilidad o el enclavamiento. En lo que a mí respecta, los subprocesos múltiples sin bloqueo son para expertos en subprocesos reales, de los cuales no soy uno. Si Joe Duffy y su equipo construyen bibliotecas agradables que paralizarán las cosas sin tanto bloqueo como lo que yo construiría, eso es fabuloso, y lo usaré en un abrir y cerrar de ojos, pero cuando hago el subprocesamiento yo mismo, trato de mantenlo simple.