whatwg - Cual std:: sync:: atomic:: Ordering to use?
whatwg español (1)
No soy un experto en esto, y es realmente complicado, así que no dude en criticar mi publicación. Como lo señala mdh.heydari, cppreference.com tiene mucha mejor documentación de pedidos que Rust (C ++ tiene una API casi idéntica).
Para tu pregunta
Necesitaría utilizar pedidos "liberados" en su productor y pedidos "adquiridos" en su consumidor. Esto asegura que la mutación de datos ocurra antes de que AtomicBool
se establezca en verdadero.
Si su cola es asíncrona, entonces el consumidor deberá seguir intentando leer en un bucle, ya que el productor podría interrumpirse entre la configuración de AtomicBool
y poner algo en la cola.
Si el código del productor puede ejecutarse varias veces antes de que se ejecute el cliente, entonces no puede usar RefCell
porque podrían mutar los datos mientras el cliente lo está leyendo. De lo contrario está bien.
Hay otras formas mejores y más sencillas de implementar este patrón, pero supongo que solo lo estaba dando como ejemplo.
¿Qué son los pedidos?
Los diferentes ordenamientos tienen que ver con que otro hilo ve que ocurre cuando se produce una operación atómica. Los compiladores y las CPUs normalmente pueden reordenar las instrucciones para optimizar el código, y los pedidos afectan la cantidad de reordenación de las instrucciones.
Simplemente siempre puede usar SeqCst
, lo que básicamente garantiza que todos verán que la instrucción se produjo en cualquier lugar en que la colocó en relación con otras instrucciones, pero en algunos casos, si especifica un orden menos restrictivo, LLVM y la CPU pueden optimizar mejor su código.
Debe pensar que estos pedidos se aplican a una ubicación de memoria (en lugar de aplicar a una instrucción).
Tipos de pedidos
Pedidos relajados
No hay restricciones, además de que cualquier modificación de la ubicación de la memoria es atómica (por lo que puede suceder completamente o no ocurrir). Esto está bien para algo como un contador si los valores recuperados por / establecidos por subprocesos individuales no importan mientras sean atómicos.
Adquirir pedidos
Esta restricción dice que cualquier lectura de variable que ocurra en su código después de que se aplique "adquirir" no se puede reordenar antes de que ocurra. Entonces, diga en su código que lee alguna ubicación de memoria compartida y obtiene el valor X
, que se almacenó en esa ubicación de memoria en el momento T
, y luego aplica la restricción de "adquirir". Cualquier ubicación de memoria que lea después de aplicar la restricción tendrá el valor que tenían en el momento T
o posterior.
Esto es probablemente lo que la mayoría de la gente espera que ocurra de manera intuitiva, pero como la CPU y el optimizador pueden reordenar las instrucciones siempre que no cambien el resultado, no está garantizado.
Para que "adquirir" sea útil, debe emparejarse con "liberación", porque de lo contrario no hay garantía de que el otro hilo no reordenara sus instrucciones de escritura que debían ocurrir en el momento T
hasta un momento anterior.
Adquirir la lectura del valor del indicador que está buscando significa que no verá un valor obsoleto en algún otro lugar que haya sido cambiado por una escritura antes de la versión de lanzamiento en el indicador.
Orden de liberación
Esta restricción dice que cualquier escritura de variable que ocurra en su código antes de que se aplique "release" no se puede reordenar para que ocurra después de ella. Entonces, diga en su código que escribe en unas pocas ubicaciones de memoria compartida y luego establezca alguna ubicación de memoria t en el momento T
, y luego aplique la restricción de "liberación". Cualquier escritura que aparezca en su código antes de que se aplique el "lanzamiento" se garantiza que haya ocurrido antes.
De nuevo, esto es lo que la mayoría de la gente espera que ocurra de manera intuitiva, pero no está garantizado sin restricciones.
Si el otro hilo que intenta leer el valor X
no usa "adquirir", entonces no se garantiza que vea el nuevo valor con respecto a los cambios en otros valores variables. Por lo tanto, podría obtener el nuevo valor, pero podría no ver nuevos valores para otras variables compartidas. También tenga en cuenta que las pruebas son difíciles . En la práctica, algún hardware no mostrará el reordenamiento con algún código inseguro, por lo que los problemas pueden pasar desapercibidos.
Jeff Preshing escribió una buena explicación de cómo adquirir y lanzar semánticas , así que lea esto si no está claro.
Pedidos de acqrel
Esto hace tanto el pedido de Acquire
como el de Release
(es decir, se aplican ambas restricciones). No estoy seguro de cuándo es necesario, podría ser útil en situaciones con 3 o más subprocesos si un Release
, un Acquire
y algunos hacen ambos, pero no estoy realmente seguro.
Pedido de SeqCst
Esta es la opción más restrictiva y, por lo tanto, más lenta. Obliga a que los accesos a la memoria aparezcan en un orden idéntico a cada subproceso. Esto requiere una instrucción MFENCE
en x86 en todas las escrituras a variables atómicas (barrera de memoria completa, incluida la carga de la tienda), mientras que los ordenamientos más débiles no lo hacen. (Las cargas SeqCst no requieren una barrera en x86, como se puede ver en esta salida del compilador de C ++ ).
Los accesos de lectura-modificación-escritura, como el incremento atómico o la comparación e intercambio, se realizan en x86 con instrucciones de lock
, que ya son barreras de memoria completas. Si le importa compilar en un código eficiente en objetivos que no sean x86, tiene sentido evitar SeqCst cuando pueda, incluso para operaciones de lectura-modificación-escritura atómicas. Sin embargo, hay casos en que es necesario .
Para obtener más ejemplos de cómo la semántica atómica se convierte en ASM, vea este conjunto más amplio de funciones simples en las variables atómicas de C ++ . Sé que esta es una pregunta de Rust, pero se supone que básicamente tiene la misma API que C ++. godbolt puede apuntar a x86, ARM, ARM64 y PowerPC. Curiosamente, ARM64 tiene instrucciones de adquisición de carga ( ldar
) y liberación de tienda ( stlr
), por lo que no siempre tiene que usar instrucciones de barrera separadas.
Por cierto, las CPU x86 siempre están "fuertemente ordenadas" por defecto, lo que significa que siempre actúan como si al menos el modo AcqRel
estuviera configurado. Por lo tanto, para x86, el "pedido" solo afecta el comportamiento del optimizador de LLVM. ARM, por otro lado, está débilmente ordenado. Relaxed
se establece de forma predeterminada, para permitir que el compilador tenga total libertad para reordenar las cosas, y para no requerir instrucciones de barrera adicionales en las CPU de orden débil.
Todos los métodos de std::sync::atomic::AtomicBool
toman un pedido de memoria (relajado, liberación, adquisición, AcqRel y SeqCst), que no he usado antes. ¿Bajo qué circunstancias deberían usarse estos valores? La documentación utiliza términos confusos de "carga" y "almacenamiento" que realmente no entiendo. Por ejemplo:
Un subproceso productor muta algún estado mantenido por un Mutex
, luego llama a std::sync::atomic::AtomicBool :: compare_and_swap(false, true, ordering)
(para unir las invalidaciones), y si se intercambia, envía un mensaje de "invalidación" a una cola concurrente (por ejemplo, mpsc
o Winapi PostMessage
). Un subproceso de consumidor restablece el AtomicBool
, lee de la cola y lee el estado en poder del Mutex. ¿Puede el productor usar un pedido relajado porque está precedido por un mutex, o debe usar Release? ¿El consumidor puede usar store(false, Relaxed)
, o debe usar compare_and_swap(true, false, Acquire)
para recibir los cambios del mutex?
¿Qué pasa si el productor y el consumidor comparten un RefCell
lugar de un Mutex
?