Pregunta

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

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

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

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

¿Fue útil?

Solución

Lo peor (en realidad no funcionará)

  

Cambie el modificador de acceso de counter a public volatile

Como han mencionado otras personas, 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 en caché los datos y reordenarán las instrucciones.

Si es no volátil , 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 que debe estar lock en cualquier otro lugar donde acceda a this.counter ). Evita que otros subprocesos ejecuten cualquier otro código protegido por locker . El uso de bloqueos también previene los problemas de reordenación de múltiples CPU como se mencionó 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 efectúa la lectura, el incremento y la escritura en 'un solo 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 suele ser literalmente una única instrucción de CPU).

Sin embargo, no estoy del todo seguro de si soluciona el problema de otras CPU, o si también necesita combinar la volatilidad con el incremento.

Notas entrelazadas:

  1. LOS MÉTODOS RELACIONADOS 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 puede reordenar.
  3. Los métodos entrelazados no necesitan o incluso no admiten el acceso a un campo volátil , ya que la volatilidad se coloca a medias alrededor de las operaciones en un campo determinado y entrelazada usa la cerca completa.

Nota a pie de página: para qué sirve la volatilidad.

Como volatile no evita este tipo de problemas de multihilo, ¿para qué sirve? Un buen ejemplo es decir que tiene dos hilos, uno que siempre escribe en una variable (por ejemplo, queueLength ), y otro que siempre se lee en 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 utilizar un bloqueo.

Otros consejos

EDITAR: Como se señaló en los comentarios, en estos días estoy feliz de usar Interlocked para los casos de una variable única donde está < em> 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 es obviamente correcto 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.

" volátil " 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. lee
  2. incremento
  3. escribe

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

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

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

por ejemplo

while (m_Var)
{ }

si m_Var se configura como 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á) al verificarlo con un registro de CPU (por ejemplo, EAX porque eso fue en lo que 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 ser almacenado en caché, no lo 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 genera 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 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. Los resultados siempre están garantizados para retirarse en orden; de lo contrario, ¡estamos llenos!

Con el Incremento interbloqueado, el procesador necesita salir, buscar el valor de la dirección dada, 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 no haya otro. Los procesadores pueden modificar su valor.

Con volatile, aún terminará con solo 1 instrucción (asumiendo que el JIT es 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 de nuevo 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

p.s. ¿Qué motivó esta respuesta tan tardía? Todas las respuestas fueron tan descaradamente incorrectas (especialmente la que está marcada como respuesta) en su explicación que solo tuve que aclarar esto para cualquier persona que lea esto. shrugs

p.p.s. 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 se ejecutaría 24-7 en x86 / x64 puede no ejecutarse 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 .

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. Por lo tanto, no hay posibilidad de interbloqueo o espera.

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

Volatile 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 de 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- informática / 210604448

el bloqueo (...) funciona, pero puede bloquear un subproceso y podría provocar 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.

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

Me gustaría agregar a lo mencionado en las otras respuestas, la diferencia entre volatile , Interlocked y lock :

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

  • Tipos de referencia.
  • Tipos de punteros (en un contexto inseguro). Tenga en cuenta que aunque el puntero en sí puede ser volátil, el objeto al que apunta no puede. En otra palabras, no puede declarar un " puntero " ser " volátil " ;.
  • Tipos simples 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, int , o uint .
  • Los parámetros de tipo genérico conocidos como tipos de referencia.
  • IntPtr y UIntPtr .

Otros tipos , incluidos double y long , no se pueden marcar " volatile " porque las lecturas y escrituras en campos de esos tipos no se pueden garantizar ser atómico Para proteger el acceso multihilo a esos tipos de campos, use los miembros de la clase Interlocked o proteja el acceso usando la Declaración de lock .

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top