c++ - sistema - ¿Se requiere una operación de barrera de memoria o atómica en un bucle de espera de ocupado?
sincronización de procesos en informatica (3)
Considere la siguiente implementación de spin_lock()
, originalmente de esta respuesta :
void spin_lock(volatile bool* lock) {
for (;;) {
// inserts an acquire memory barrier and a compiler barrier
if (!__atomic_test_and_set(lock, __ATOMIC_ACQUIRE))
return;
while (*lock) // no barriers; is it OK?
cpu_relax();
}
}
Lo que ya sé:
-
volatile
evita que el compilador optimice el*lock
releer en cada iteración del bucle while; - inserciones
volatile
ni memoria ni barreras de compilación ; - tal implementación realmente funciona en GCC para
x86
(por ejemplo, en el kernel de Linux) y algunas otras arquitecturas; - se requiere al menos una barrera de memoria y compilador en la
spin_lock()
despin_lock()
para una arquitectura genérica; este ejemplo los inserta en__atomic_test_and_set()
.
Preguntas:
¿Es lo suficientemente
volatile
aquí o hay arquitecturas o compiladores en los que se requiere una barrera o compilación de memoria o compilación en el bucle while?1.1 ¿De acuerdo con los estándares de
C++
?1.2 En la práctica, para arquitecturas y compiladores conocidos, específicamente para GCC y plataformas que soporta?
- ¿Esta implementación es segura en todas las arquitecturas compatibles con GCC y Linux? (Es al menos ineficiente en algunas arquitecturas, ¿verdad?)
- ¿Es seguro el bucle while de acuerdo con
C++11
y su modelo de memoria?
Hay varias preguntas relacionadas, pero no pude construir una respuesta explícita e inequívoca de ellas:
Q: barrera de memoria en un solo hilo
En principio: sí, si la ejecución del programa se mueve de un núcleo a otro, es posible que no vea todas las escrituras que se produjeron en el núcleo anterior.
Q: barrera de memoria y vaciado de caché
En casi todas las arquitecturas modernas, los cachés (como los cachés L1 y L2) están garantizados de forma coherente por el hardware. No es necesario vaciar ningún caché para hacer que la memoria sea visible para otras CPU.
P: ¿Es correcta y óptima mi implementación de bloqueo de giro?
P: ¿Las cerraduras de giro siempre requieren una barrera de memoria? ¿Es caro girar en una barrera de memoria?
P: ¿Espera que las futuras generaciones de CPU no sean coherentes con el caché?
- ¿Es lo suficientemente volátil aquí o hay arquitecturas o compiladores en los que se requiere una barrera o compilación de memoria o compilación en el bucle while?
¿Verá el cambio el código volátil? Sí, pero no necesariamente tan rápido como si hubiera una barrera de memoria. En algún momento, ocurrirá alguna forma de sincronización, y el nuevo estado se leerá de la variable, pero no hay garantías de cuánto ha sucedido en otras partes del código.
1.1 ¿De acuerdo con los estándares de C ++?
Desde cppreference: memory_order
Es el modelo de memoria y el orden de memoria lo que define el hardware generalizado en el que el código necesita trabajar. Para que un mensaje pase entre subprocesos de ejecución, es necesario que ocurra una relación entre subprocesos antes de suceder. Esto requiere o ...
- A sincroniza con B
- A tiene una operación estándar :: atómica antes de B
- A se sincroniza indirectamente con B (a través de X).
- A está secuenciada antes de X, que entre hilos sucede antes de B
- Un interthread sucede antes de X y X interthread sucede antes de B.
Como no está realizando ninguno de esos casos, habrá formas de su programa en las que, en algún hardware actual, puede fallar.
En la práctica, el final de un intervalo de tiempo hará que la memoria se vuelva coherente, o cualquier forma de barrera en el hilo que no es spinlock asegurará que se vacíen los cachés.
No estoy seguro sobre las causas de la lectura volátil obteniendo el "valor actual".
1.2 En la práctica, para arquitecturas y compiladores conocidos, específicamente para GCC y plataformas que soporta?
Como el código no es consistente con la CPU generalizada, desde C++11
es probable que este código no funcione con las versiones de C ++ que intentan adherirse a la norma.
De cppreference: calificadores volátiles constantes El acceso volátil impide que las optimizaciones muevan el trabajo desde antes hasta después, y desde después hasta antes.
"Esto hace que los objetos volátiles sean adecuados para la comunicación con un manejador de señales, pero no con otro hilo de ejecución"
Por lo tanto, una implementación debe garantizar que las instrucciones se lean desde la ubicación de la memoria en lugar de cualquier copia local. Pero no tiene que asegurarse de que la escritura volátil se vacíe a través de los cachés para producir una vista coherente en todas las CPU. En este sentido, no hay límite de tiempo sobre cuánto tiempo después de que una escritura en una variable volátil se vuelva visible a otro hilo.
También vea kernel.org por qué volatile está casi siempre equivocado en el kernel
¿Esta implementación es segura en todas las arquitecturas compatibles con GCC y Linux? (Es al menos ineficiente en algunas arquitecturas, ¿verdad?)
No hay garantía de que el mensaje volátil salga del hilo que lo establece. Así que no es realmente seguro. En linux puede ser seguro.
¿Es seguro el bucle while de acuerdo con C ++ 11 y su modelo de memoria?
No, ya que no crea ninguna de las primitivas de mensajería entre subprocesos.
De la página de Wikipedia sobre barreras de memoria :
... Otras arquitecturas, como el Itanium, proporcionan barreras de memoria de "adquisición" y "liberación" separadas que abordan la visibilidad de las operaciones de lectura después de la escritura desde el punto de vista de un lector (receptor) o escritor (fuente) respectivamente .
Para mí, esto implica que Itanium requiere una cerca adecuada para hacer que las lecturas / escrituras sean visibles para otros procesadores, pero de hecho, esto puede ser solo para fines de pedido. La pregunta, creo, realmente se reduce a:
¿Existe una arquitectura donde un procesador nunca pueda actualizar su caché local si no se le indica que lo haga? No sé la respuesta, pero si formuló la pregunta de esta forma, alguien más podría hacerlo. En tal arquitectura, su código potencialmente entra en un bucle infinito donde la lectura de *lock
siempre ve el mismo valor.
En términos de la legalidad general de C ++, la única prueba atómica y el conjunto en su ejemplo no son suficientes, ya que implementa solo una única cerca que le permitirá ver el estado inicial del *lock
al ingresar al ciclo while pero no ver cuando cambia (lo que resulta en un comportamiento indefinido, ya que estás leyendo una variable que se cambia en otro hilo sin sincronización), por lo que la respuesta a tu pregunta (1.1 / 3) es no .
Por otro lado, en la práctica, la respuesta a (1.2 / 2) es sí (dada gcc.gnu.org/onlinedocs/gcc/Volatiles.html ), siempre que la arquitectura garantice la coherencia de la memoria caché sin vallas de memoria explícitas, lo cual es cierto para x86 y probablemente para muchas arquitecturas, pero no puedo dar una respuesta definitiva sobre si es cierto para todas las arquitecturas que admite GCC. Sin embargo, generalmente no es prudente confiar a sabiendas en un comportamiento particular del código que es un comportamiento técnicamente indefinido de acuerdo con las especificaciones del idioma, especialmente si es posible obtener el mismo resultado sin hacerlo.
Incidentalmente, dado que existe memory_order_relaxed
, parece haber pocas razones para no usarlo en este caso en lugar de tratar de optimizar a mano utilizando lecturas no atómicas, es decir, cambiando el ciclo while en su ejemplo para:
while (atomic_load_explicit(lock, memory_order_relaxed)) {
cpu_relax();
}
En x86_64, por ejemplo, la carga atómica se convierte en una instrucción de mov
regular y la salida de ensamblaje optimizada es esencialmente la misma que para su ejemplo original.
Esto es importante: en C ++, volatile
no tiene nada que ver con la concurrencia. El propósito de volatile
es decirle al compilador que no debe optimizar los accesos al objeto afectado. No le dice nada a la CPU, principalmente porque la CPU ya sabría si la memoria sería volatile
o no. El propósito de volatile
es efectivamente lidiar con la E / S asignada en memoria.
El estándar de C ++ es muy claro en la sección 1.10 [intro.multithread] que el acceso no sincronizado a un objeto que se modifica en un hilo y se accede (modificado o leído) en otro hilo es un comportamiento indefinido. Las primitivas de sincronización que evitan el comportamiento indefinido son componentes de la biblioteca como las clases atómicas o mutexes. Esta cláusula menciona la volatile
solo en el contexto de las señales (es decir, como volatile sigatomic_t
) y en el contexto del progreso hacia adelante (es decir, que un hilo finalmente hará algo que tenga un efecto observable como acceder a un objeto volatile
o hacer E / S) . No hay ninguna mención de volatile
junto con la sincronización.
Por lo tanto, la evaluación no sincronizada de una variable compartida en subprocesos conduce a un comportamiento indefinido. Si se declara volatile
o no, no importa para este comportamiento indefinido.