c# - Volatile vs. Interlocked vs. Lock




multithreading locking (6)

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.

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?


" 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.


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.



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.







interlocked