Estoy luchando con la Sección 5.1.2.4 del Estándar C11, en particular la semántica de Release / Acquire. Observo que https://preshing.com/20120913/acquire-and-release-semantics/ (entre otros) establece que:
... La semántica de liberación evita el reordenamiento de memoria de la liberación de escritura con cualquier operación de lectura o escritura que la preceda en orden de programa.
Entonces, para lo siguiente:
typedef struct test_struct
{
_Atomic(bool) ready ;
int v1 ;
int v2 ;
} test_struct_t ;
extern void
test_init(test_struct_t* ts, int v1, int v2)
{
ts->v1 = v1 ;
ts->v2 = v2 ;
atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}
extern int
test_thread_1(test_struct_t* ts, int v2)
{
int v1 ;
while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
ts->v2 = v2 ; // expect read to happen before store/release
v1 = ts->v1 ; // expect write to happen before store/release
atomic_store_explicit(&ts->ready, true, memory_order_release) ;
return v1 ;
}
extern int
test_thread_2(test_struct_t* ts, int v1)
{
int v2 ;
while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
ts->v1 = v1 ;
v2 = ts->v2 ; // expect write to happen after store/release in thread "1"
atomic_store_explicit(&ts->ready, false, memory_order_release) ;
return v2 ;
}
donde se ejecutan esos:
> in the "main" thread: test_struct_t ts ;
> test_init(&ts, 1, 2) ;
> start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
> start thread "1" which does: r1 = test_thread_1(&ts, 4) ;
Por lo tanto, esperaría que el hilo "1" tenga r1 == 1 y el hilo "2" tenga r2 = 4.
Esperaría eso porque (siguiendo los párrafos 16 y 18 de la sección 5.1.2.4):
- todas las lecturas y escrituras (no atómicas) se "secuencian antes" y, por lo tanto, "suceden antes" de la escritura / liberación atómica en el hilo "1",
- que "entre hilos pasa antes" la lectura / adquisición atómica en el hilo "2" (cuando dice 'verdadero'),
- que a su vez está "secuenciado antes" y, por lo tanto, "sucede antes" de las lecturas y escrituras (no atómicas) (en el hilo "2").
Sin embargo, es totalmente posible que no haya entendido el estándar.
Observo que el código generado para x86_64 incluye:
test_thread_1:
movzbl (%rdi),%eax -- atomic_load_explicit(&ts->ready, memory_order_acquire)
test $0x1,%al
jne <test_thread_1> -- while is true
mov %esi,0x8(%rdi) -- (W1) ts->v2 = v2
mov 0x4(%rdi),%eax -- (R1) v1 = ts->v1
movb $0x1,(%rdi) -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
retq
test_thread_2:
movzbl (%rdi),%eax -- atomic_load_explicit(&ts->ready, memory_order_acquire)
test $0x1,%al
je <test_thread_2> -- while is false
mov %esi,0x4(%rdi) -- (W2) ts->v1 = v1
mov 0x8(%rdi),%eax -- (R2) v2 = ts->v2
movb $0x0,(%rdi) -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
retq
Y siempre que R1 y X1 sucedan en ese orden, esto da el resultado que espero.
Pero mi comprensión de x86_64 es que las lecturas ocurren en orden con otras lecturas y las escrituras ocurren en orden con otras escrituras, pero las lecturas y escrituras pueden no ocurrir en orden entre sí. Lo que implica que es posible que X1 suceda antes de R1, e incluso que X1, X2, W2, R1 sucedan en ese orden, creo. [Esto parece desesperadamente improbable, pero si R1 se retrasó por algunos problemas de caché?]
Por favor: ¿qué no estoy entendiendo?
Observo que si cambio las cargas / tiendas de ts->readya memory_order_seq_cst, el código generado para las tiendas es:
xchg %cl,(%rdi)
lo cual es consistente con mi comprensión de x86_64 y dará el resultado que espero.
8.2.3.3 Stores Are Not Reordered With Earlier Loads. Por lo tanto, su compilador está traduciendo correctamente su código (qué sorprendente), de modo que su código sea efectivamente completamente secuencial y no ocurra nada interesante al mismo tiempo.