thread lock for español c# multithreading thread-safety shared-memory memory-barriers

c# - lock - ¿Por qué necesito una barrera de memoria?



lock c# español (2)

El ejemplo no está claro por dos razones:

  1. Es demasiado simple para mostrar completamente lo que está sucediendo con las vallas.
  2. Albahari incluye requisitos para arquitecturas que no sean x86. Vea MSDN : "MemoryBarrier es necesario solo en sistemas multiprocesador con orden de memoria débil (por ejemplo, un sistema que emplea múltiples procesadores Intel Itanium [que Microsoft ya no admite])".

Si considera lo siguiente, se vuelve más claro:

  1. Una barrera de memoria (las barreras completas aquí - .Net no proporciona una media barrera) evita que las instrucciones de lectura / escritura salten de la valla (debido a varias optimizaciones). Esto nos garantiza el código después de que la valla se ejecutará después del código antes de la valla.
  2. "Esta operación de serialización garantiza que cada instrucción de carga y almacenamiento que precede en el orden del programa, la instrucción MFENCE esté visible globalmente antes de que cualquier instrucción de carga o almacenamiento que sigue a la instrucción MFENCE sea visible globalmente". Mira here .
  3. Las CPU x86 tienen un modelo de memoria sólido y las escrituras de garantía aparecen coherentes con todos los hilos / núcleos (por lo tanto, las barreras n.º 2 y n.º 3 no son necesarias en x86). Sin embargo, no se garantiza que las lecturas y escrituras permanezcan en secuencia codificada, de ahí la necesidad de las barreras # 1 y # 4.
  4. Las barreras de memoria son ineficientes y no necesitan ser utilizadas (vea el mismo artículo de MSDN). Yo personalmente uso Enclavado y volátil (¡¡¡¡¡¡¡¡¡asegúrese de saber cómo usarlo correctamente !!), que funcionan de manera eficiente y son fáciles de entender.

PD. Este artículo explica el funcionamiento interno de x86 muy bien.

C # 4 in a Nutshell (muy recomendable por cierto) utiliza el siguiente código para demostrar el concepto de MemoryBarrier (suponiendo que A y B se ejecutaron en diferentes subprocesos):

class Foo{ int _answer; bool complete; void A(){ _answer = 123; Thread.MemoryBarrier(); // Barrier 1 _complete = true; Thread.MemoryBarrier(); // Barrier 2 } void B(){ Thread.MemoryBarrier(); // Barrier 3; if(_complete){ Thread.MemoryBarrier(); // Barrier 4; Console.WriteLine(_answer); } } }

mencionan que las barreras 1 y 4 impiden que este ejemplo escriba 0 y las barreras 2 y 3 proporcionan una garantía de frescura : aseguran que si B funcionaba después de A, la lectura _completa sería verdadera .

Realmente no lo entiendo. Creo que entiendo por qué las barreras 1 y 4 son necesarias: no queremos que la escritura en _ respuesta se optimice y se coloque después de la escritura en _completar (barrera 1) y tenemos que asegurarnos de que _ respuesta no esté en la memoria caché (barrera 4) . También creo que entiendo por qué Barrier 3 es necesario: si A se ejecutó hasta justo después de escribir _complete = true , B aún tendría que actualizar _complete para leer el valor correcto.

¡No entiendo por qué necesitamos la Barrera 2! Una parte de mí dice que es porque tal vez el Tema 2 (que se ejecuta en B) ya se ejecutó hasta (pero sin incluir) si (_completo) y entonces necesitamos asegurarnos de que _completar se actualice.

Sin embargo, no veo cómo esto ayuda. ¿Todavía no es posible que _complete se establezca en verdadero en A, pero el método B verá una versión en caché (falsa) de _completar ? Es decir, si el Tema 2 ejecutó el método B hasta después del primer MemoryBarrier y luego el Tema 1 ejecutó el método A hasta _completar = verdadero pero no más, y luego el Tema 1 reanudó y probó si (_completo) - ¿podría eso dar como resultado falso ?


La barrera # 2 garantiza que la escritura en _complete se compromete de inmediato. De lo contrario, podría permanecer en un estado en cola, lo que significa que la lectura de _complete en B no vería el cambio causado por A , aunque B efectivamente utilizó una lectura volátil.

Por supuesto, este ejemplo no acaba de hacer justicia al problema porque A no hace nada más después de escribir en _complete que significa que la escritura se realizará inmediatamente de todos modos, ya que el hilo termina temprano.

La respuesta a su pregunta de si el if aún puede ser false es sí por exactamente las razones que usted indicó. Pero, observe lo que dice el autor con respecto a este punto.

Las barreras 1 y 4 evitan que este ejemplo escriba "0". Las barreras 2 y 3 proporcionan una garantía de frescura: aseguran que si B funcionaba después de A , la lectura _completa sería verdadera.

El énfasis en "si B corrió después de A" es mío. Ciertamente podría ser el caso de que los dos hilos se entrelazan. Pero, el autor estaba ignorando este escenario, presumiblemente para hacer su punto con respecto a cómo funciona Thread.MemoryBarrier más simple.

Por cierto, me costó crear un ejemplo en mi máquina donde las barreras # 1 y # 2 habrían alterado el comportamiento del programa. Esto se debe a que el modelo de memoria con respecto a las escrituras era fuerte en mi entorno. Quizás, si tuviera una máquina multiprocesador, estuviera usando Mono, o tuviera alguna configuración diferente, podría haberlo demostrado. Por supuesto, fue fácil demostrar que eliminar barreras # 3 y # 4 tuvo un impacto.