Volátil vs. Interbloqueado vs. bloqueo

671

Digamos que una clase tiene un public int countercampo al que acceden varios subprocesos. Esto intsolo se incrementa o disminuye.

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

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

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

núcleo
fuente
Lea la referencia de Threading en C # . Cubre los entresijos de su pregunta. Cada uno de los tres tiene diferentes propósitos y efectos secundarios.
spoulson
1
simple-talk.com/blogs/2012/01/24/… puede ver el uso de volitable en matrices, no lo entiendo completamente, pero es otra referencia a lo que hace.
eran otzap
50
Esto es como decir "He descubierto que el sistema de rociadores nunca está activado, por lo que lo voy a quitar y reemplazar por alarmas de humo" La razón para no hacer esto es porque es increíblemente peligroso y casi no te da ningún beneficio . Si tiene tiempo para cambiar el código, ¡ encuentre la manera de hacerlo menos multiproceso ! ¡No encuentre la manera de hacer que el código multiproceso sea más peligroso y se rompa fácilmente!
Eric Lippert
1
Mi casa tiene rociadores y detectores de humo. Al incrementar un contador en un hilo y leerlo en otro, parece que necesita tanto un bloqueo (o un enclavamiento) como la palabra clave volátil. ¿Verdad?
yoyo
2
@yoyo No, no necesitas ambos.
David Schwartz

Respuestas:

859

Lo peor (en realidad no funcionará)

Cambiar el modificador de acceso de counterapublic volatile

Como han mencionado otras personas, esto por sí solo no es realmente seguro en absoluto. El punto volatilees que varios subprocesos que se ejecutan en varias CPU pueden almacenar datos en caché y volverán a ordenar las instrucciones.

Si no es así 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 así 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 está tratando de evitar.

Segundo mejor:

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

Esto es seguro (siempre y cuando recuerdes en lockcualquier otro lugar al que accedas this.counter). Impide que cualquier otro subproceso ejecute cualquier otro código que esté protegido por locker. El uso de bloqueos también evita los problemas de reordenamiento de múltiples CPU como se indicó anteriormente, lo cual es excelente.

El problema es que el bloqueo es lento, y si vuelve a usarlo lockeren otro lugar que no está realmente relacionado, puede terminar bloqueando sus otros hilos sin ningún motivo.

Mejor

Interlocked.Increment(ref this.counter);

Esto es seguro, ya que efectivamente hace la lectura, el incremento y la escritura en 'un golpe' que no se puede interrumpir. Debido a esto, no afectará a ningún otro código, y tampoco es necesario que recuerde bloquear en otro lugar. También es muy rápido (como dice MSDN, en las CPU modernas, esto es literalmente una sola instrucción de CPU).

Sin embargo, no estoy del todo seguro si se soluciona con otras CPU que reordenan cosas, o si también necesita combinar volátiles con el incremento.

Notas entrelazadas:

  1. LOS MÉTODOS ENCLAVADOS SON CONCURRENTEMENTE SEGUROS EN CUALQUIER NÚMERO DE NÚCLEOS O CPU.
  2. Los métodos enclavados aplican una cerca completa alrededor de las instrucciones que ejecutan, por lo que no se reordena.
  3. Los métodos enclavados no necesitan o ni siquiera admiten el acceso a un campo volátil , ya que el volátil se coloca media cerca alrededor de las operaciones en un campo determinado y enclavado está usando la cerca completa.

Nota al pie: Para qué es volátil en realidad es bueno.

Como volatileno evita este tipo de problemas de subprocesos múltiples, ¿para qué sirve? Un buen ejemplo es decir que tiene dos hilos, uno que siempre escribe en una variable (digamos queueLength) y otro que siempre lee de esa misma variable.

Si queueLengthno es volátil, el hilo A puede escribir cinco veces, pero el hilo B puede ver que esas escrituras están retrasadas (o incluso potencialmente en el orden incorrecto).

Una solución sería bloquear, pero también podría usar volátiles en esta situación. Esto aseguraría que el hilo B siempre verá lo más actualizado que el hilo 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, deberá ir a Operaciones enclavadas o usar un Bloqueo.

Orion Edwards
fuente
29
"No estoy del todo seguro ... si también necesitas combinar volátiles con el incremento". No se pueden combinar AFAIK, ya que no podemos pasar un volátil por ref. Gran respuesta por cierto.
Hosam Aly
41
Muchas gracias! Su nota al pie de página sobre "Para qué lo volátil es realmente bueno" es lo que estaba buscando y confirmó cómo quiero usarlo.
Jacques Bosch
77
En otras palabras, si un var se declara como volátil, el compilador asumirá que el valor del var no seguirá siendo el mismo (es decir, volátil) cada vez que su código lo encuentre. Entonces, en un bucle como: while (m_Var) {}, y m_Var se establece en falso en otro hilo, el compilador simplemente no verificará lo que ya está en un registro que se cargó previamente con el valor de m_Var, sino que lee el valor de m_Var de nuevo. Sin embargo, no significa que no declarar volátil hará que el bucle continúe infinitamente; especificar volátil solo garantiza que no lo hará si m_Var se establece en falso en otro subproceso.
Zach vio el
35
@Zach Saw: Bajo el modelo de memoria para C ++, volátil es como lo has descrito (básicamente útil para la memoria mapeada por dispositivo y no mucho más). Según el modelo de memoria para el CLR (esta pregunta está etiquetada con C #) es que volátil insertará barreras de memoria alrededor de las lecturas y escrituras en esa ubicación de almacenamiento. Las barreras de memoria (y las variaciones especiales bloqueadas de algunas instrucciones de ensamblaje) le dicen al procesador que no reordene las cosas, y son bastante importantes ...
Orion Edwards
19
@ZachSaw: un campo volátil en C # impide que el compilador C # y el compilador jit realicen ciertas optimizaciones que almacenarían en caché el valor. También ofrece ciertas garantías sobre el orden en que se puede observar que las lecturas y escrituras se encuentran en varios subprocesos. Como detalle de implementación, puede hacerlo mediante la introducción de barreras de memoria en lecturas y escrituras. La semántica precisa garantizada se describe en la especificación; tenga en cuenta que la especificación no garantiza que todos los hilos observarán un orden consistente de todas las escrituras y lecturas volátiles .
Eric Lippert
147

EDITAR: Como se señaló en los comentarios, en estos días estoy feliz de usar Interlockedpara los casos de una sola variable donde obviamente está bien. Cuando se vuelva más complicado, volveré a bloquear ...

El uso volatileno ayudará cuando necesite incrementar, porque 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 solo bloqueo: es más fácil acertar de una manera que obviamente es correcta que la volatilidad o el enclavamiento. En lo que a mí respecta, el subprocesamiento múltiple sin bloqueo es para expertos en subprocesos reales, de los cuales no soy uno. Si Joe Duffy y su equipo construyen bibliotecas agradables que pondrán en paralelo las cosas sin tanto bloqueo como algo que yo construiría, es fabuloso, y lo usaré en un abrir y cerrar de ojos, pero cuando estoy enhebrando, trato de mantenlo simple.

Jon Skeet
fuente
16
+1 por asegurarme de olvidarme de la codificación sin bloqueo a partir de ahora.
Xaqron
55
los códigos sin bloqueo definitivamente no están realmente libres de bloqueo, ya que se bloquean en algún momento, ya sea en el bus (FSB) o en el nivel de interCPU, todavía hay una multa que tendría que pagar. Sin embargo, el bloqueo en estos niveles inferiores es generalmente más rápido siempre que no sature el ancho de banda del lugar donde se produce el bloqueo.
Zach vio el
2
No hay nada de malo en Interlocked, es exactamente lo que buscas y más rápido que un bloqueo completo ()
Jaap
55
@Jaap: Sí, en estos días que podría utilizar entrelazado para una genuina solo contador. Simplemente no quisiera comenzar a perder el tiempo tratando de resolver las interacciones entre múltiples actualizaciones sin bloqueo de variables.
Jon Skeet
66
@ZachSaw: su segundo comentario dice que las operaciones entrelazadas se "bloquean" en algún momento; el término "bloqueo" generalmente implica que una tarea puede mantener el control exclusivo de un recurso durante un período de tiempo ilimitado; La principal ventaja de la programación sin bloqueo es que evita el peligro de que los recursos se vuelvan inutilizables como resultado de que la tarea de propiedad se bloquee. La sincronización de bus utilizada por la clase entrelazada no es solo "generalmente más rápida": en la mayoría de los sistemas tiene un tiempo limitado en el peor de los casos, mientras que los bloqueos no.
supercat
44

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

Michael Damatov
fuente
44
Dicho de otra manera, los cambios entrelazados están completamente cercados y, como tales, son atómicos. Los miembros volátiles solo están parcialmente cercados y, como tales, no se garantiza que sean seguros para subprocesos.
JoeGeeky
1
En realidad, volatileno se asegura de que la variable no esté en caché. Simplemente impone restricciones sobre cómo se puede almacenar en caché. Por ejemplo, todavía se puede almacenar en caché en cosas de la caché L2 de la CPU porque se hacen coherentes en el hardware. Todavía puede ser prefectado. Las escrituras todavía se pueden publicar en la memoria caché, etc. (Lo que creo que era a lo que Zach se refería.)
David Schwartz
42

Lo que estás buscando es el bloqueo o el incremento entrelazado.

Definitivamente, lo volátil no es lo que está buscando: simplemente le dice al compilador que trate la variable como siempre cambiante, incluso si la ruta de 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 subproceso 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 compruebe en un registro de CPU (por ejemplo, EAX porque era en lo que se buscó m_Var desde el principio) en lugar de emitir otra lectura en la ubicación de memoria de m_Var (esto puede almacenarse en caché; no sabemos y no nos importa, y ese es el punto de 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. Volátil noemita barreras de lectura / escritura como lo implican las publicaciones anteriores que dicen 'evita la reordenación'. De hecho, gracias de nuevo al protocolo MESI, tenemos la garantía de 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 la memoria 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. Siempre se garantiza que los resultados se retirarán en orden; de lo contrario, ¡estamos llenos!

Con Incremento Interbloqueado, el procesador debe salir, obtener el valor de la dirección dada, luego incrementarlo y escribirlo de nuevo, todo eso mientras tiene la propiedad exclusiva de toda la línea de caché (bloqueo xadd) para asegurarse de que ningún otro procesador pueda modificar es valioso.

Con volátil, 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 enclavada. Como puede imaginar, esto significa que otros procesadores podrían volver a escribir un valor actualizado en m_Var después de que cpuA lo haya leído. Entonces, en lugar de ahora 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 bajo bloqueo en aplicaciones 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 para cualquier otra persona que lea esto. encoge 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 ECMA de Microsoft están jodidas porque 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 se ejecutaría 24-7 en x86 / Es posible que x64 no se ejecute en absoluto en IA64, aunque Intel ha implementado un modelo de memoria similarmente fuerte para IA64) - Microsoft lo admitió ellos mismos - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

Zach vio
fuente
3
Interesante. ¿Puedes hacer referencia a esto? Felizmente votaría esto, pero publicar con un lenguaje agresivo 3 años después de una respuesta altamente votada que sea consistente con los recursos que he leído requerirá una prueba un poco más tangible.
Steven Evers
Si puede señalar a qué parte desea hacer referencia, me complacería desenterrar algunas cosas de alguna parte (dudo mucho que haya revelado los secretos comerciales de los proveedores x86 / x64, por lo que estos deberían estar fácilmente disponibles en wiki, Intel PMR (manuales de referencia del programador), blogs MSFT, MSDN o algo similar) ...
Zach Saw
2
¿Por qué alguien querría evitar que la CPU se almacene en caché? Todo el espacio inmobiliario (definitivamente no es insignificante en tamaño y costo) dedicado a realizar la coherencia de la memoria caché se desperdicia por completo si ese es el caso ... A menos que no requiera coherencia de la memoria caché, como una tarjeta gráfica, dispositivo PCI, etc., no establecería una línea de caché para escribir.
Zach vio el
44
Sí, todo lo que dices es si no 100% al menos 99% en la marca. Este sitio es (en su mayoría) bastante útil cuando tienes prisa por el desarrollo en el trabajo, pero desafortunadamente la precisión de las respuestas correspondientes al (juego de) votos no está allí. Básicamente, en stackoverflow puedes tener una idea de cuál es la comprensión popular de los lectores, no de lo que realmente es. A veces, las principales respuestas son simplemente galimatías, mitos de tipo. Y desafortunadamente, esto es lo que genera la gente que se encuentra con la lectura mientras resuelve el problema. Sin embargo, es comprensible, nadie puede saberlo todo.
user1416420
1
@BenVoigt Podría seguir y responder sobre todas las arquitecturas en las que se ejecuta .NET, pero eso tomaría algunas páginas y definitivamente no es adecuado para SO. Es mucho mejor educar a las personas basándose en el modelo de memoria subyacente de hardware .NET más utilizado que uno que sea arbitrario. Y con mis comentarios 'en todas partes', estaba corrigiendo los errores que la gente estaba cometiendo al asumir el vaciado / invalidación de la memoria caché, etc. Hicieron suposiciones sobre el hardware subyacente sin especificar qué hardware.
Zach vio
16

Las funciones entrelazadas no se bloquean. Son atómicos, lo que significa que pueden completarse sin la posibilidad de un cambio de contexto durante el incremento. Por lo tanto, no hay posibilidad de estancamiento o espera.

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

Volátil es útil si necesita escrituras en un hilo para leer en otro, y si desea que el optimizador no reordene las operaciones en una variable (porque las cosas están sucediendo en otro hilo que el optimizador no conoce). Es una elección ortogonal de cómo incrementar.

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

Lou Franco
fuente
11

El bloqueo (...) funciona, pero puede bloquear un subproceso y podría provocar un punto muerto si otro código está utilizando los mismos bloqueos de una manera incompatible.

Interlocked. * Es la forma correcta de hacerlo ... mucho menos sobrecarga ya que las CPU modernas admiten esto como una primitiva.

volátil por sí solo no es correcto. Un hilo que intenta recuperar y luego escribir un valor modificado aún podría entrar en conflicto con otro hilo que haga lo mismo.

Rob Walker
fuente
8

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 CompareExchnage pero el resultado para Incremento es similar. Interbloqueado no es necesario más rápido en un entorno de varias 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
Kenneth Xu
fuente
Sin embargo, el ejemplo de código que probó fue muuuy trivial, ¡realmente no tiene mucho sentido probarlo de esa manera! Lo mejor sería entender qué están haciendo los diferentes métodos y usar el apropiado según el escenario de uso que tenga.
Zach vio el
@Zach, cómo se discutió aquí sobre el escenario de aumentar un contador de una manera segura para subprocesos. ¿Qué otro escenario de uso tenía en mente o cómo lo probaría? Gracias por el comentario BTW.
Kenneth Xu
El punto es que es una prueba artificial. No vas a martillar la misma ubicación que a menudo en cualquier escenario del mundo real. Si es así, entonces el FSB está bloqueado (como se muestra en los cuadros de su servidor). De todos modos, mira mi respuesta en tu blog.
Zach vio el
2
Mirándolo de nuevo. Si el verdadero cuello de botella es con FSB, la implementación del monitor debe observar el mismo cuello de botella. La verdadera diferencia es que Interlocked está haciendo ocupadas esperas y reintentos, lo que se convierte en un problema real con el conteo de alto rendimiento. Al menos espero que mi comentario llame la atención de que Interlocked no siempre es la opción correcta para contar. El hecho de que la gente esté buscando alternativas lo explica bien. Necesita un sumador largo gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Kenneth Xu
8

Respaldo la respuesta de Jon Skeet y quiero agregar los siguientes enlaces para todos los que quieran saber más sobre "volátil" e Interbloqueado:

La atomicidad, la volatilidad y la inmutabilidad son diferentes, primera parte - (Fabulosas aventuras en codificación de Eric Lippert)

Atomicidad, volatilidad e inmutabilidad son diferentes, segunda parte

Atomicidad, volatilidad e inmutabilidad son diferentes, tercera parte

Sayonara Volatile - (instantánea de Wayback Machine del Weblog de Joe Duffy como apareció en 2012)

zihotki
fuente
3

Me gustaría añadir que se menciona en las otras respuestas la diferencia entre volatile, Interlockedy lock:

La palabra clave volátil se puede aplicar a campos de estos tipos :

  • Tipos de referencia
  • Tipos de puntero (en un contexto inseguro). Tenga en cuenta que aunque el puntero en sí mismo puede ser volátil, el objeto al que apunta no puede. En otras palabras, no puede declarar que un "puntero" sea "volátil".
  • Los tipos simples tales como sbyte, byte, short, ushort, int, uint, char, float, y bool.
  • Un tipo de enumeración con uno de los siguientes tipos de base: byte, sbyte, short, ushort, into uint.
  • Parámetros de tipo genérico conocidos como tipos de referencia.
  • IntPtry UIntPtr.

Otros tipos , incluidos doubley long, no se pueden marcar como "volátiles" porque no se puede garantizar que las lecturas y escrituras en campos de esos tipos sean atómicas. Para proteger el acceso de subprocesos múltiples a esos tipos de campos, use los Interlockedmiembros de la clase o proteja el acceso usando la lockinstrucción.

Vadim S.
fuente