Digamos que una clase tiene un public int counter
campo al que acceden varios subprocesos. Esto int
solo 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
counter
apublic volatile
.
Ahora que he descubierto volatile
, he estado eliminando muchas lock
declaraciones y el uso de Interlocked
. Pero, ¿hay alguna razón para no hacer esto?
c#
multithreading
locking
volatile
interlocked
núcleo
fuente
fuente
Respuestas:
Lo peor (en realidad no funcionará)
Como han mencionado otras personas, esto por sí solo no es realmente seguro en absoluto. El punto
volatile
es 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:
Esto es seguro (siempre y cuando recuerdes en
lock
cualquier otro lugar al que accedasthis.counter
). Impide que cualquier otro subproceso ejecute cualquier otro código que esté protegido porlocker
. 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
locker
en otro lugar que no está realmente relacionado, puede terminar bloqueando sus otros hilos sin ningún motivo.Mejor
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:
Nota al pie: Para qué es volátil en realidad es bueno.
Como
volatile
no 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 (digamosqueueLength
) y otro que siempre lee de esa misma variable.Si
queueLength
no 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.
fuente
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 vuelva más complicado, volveré a bloquear ...El uso
volatile
no 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.
fuente
"
volatile
" no reemplazaInterlocked.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:
Interlocked.Increment
realiza las tres partes como una sola operación atómica.fuente
volatile
no 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.)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
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 .
fuente
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
fuente
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.
fuente
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.
fuente
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)
fuente
Me gustaría añadir que se menciona en las otras respuestas la diferencia entre
volatile
,Interlocked
ylock
:La palabra clave volátil se puede aplicar a campos de estos tipos :
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
, ybool
.byte
,sbyte
,short
, ushort,int
ouint
.IntPtr
yUIntPtr
.Otros tipos , incluidos
double
ylong
, 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 losInterlocked
miembros de la clase o proteja el acceso usando lalock
instrucción.fuente