Ich habe Probleme mit Abschnitt 5.1.2.4 des C11-Standards, insbesondere mit der Semantik von Release / Acquire. Ich stelle fest, dass https://preshing.com/20120913/acquire-and-release-semantics/ (unter anderem) besagt, dass:
... Die Release-Semantik verhindert, dass die Schreibfreigabe mit einer Lese- oder Schreiboperation, die ihr in der Programmreihenfolge vorausgeht, im Speicher neu angeordnet wird.
Also für Folgendes:
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 ;
}
wo diese ausgeführt werden:
> 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) ;
Ich würde daher erwarten, dass Thread "1" r1 == 1 und Thread "2" r2 = 4 hat.
Ich würde das erwarten, weil (gemäß den Absätzen 16 und 18 von Abschnitt 5.1.2.4):
- Alle (nicht atomaren) Lese- und Schreibvorgänge werden "vor" sequenziert und daher "vor" dem atomaren Schreiben / Freigeben in Thread "1" durchgeführt.
- welches "Inter-Thread-passiert-vor" dem atomaren Lesen / Erfassen in Thread "2" (wenn es "wahr" lautet),
- was wiederum "vorher sequenziert" ist und daher "vor" dem (nicht atomaren) Lesen und Schreiben (im Thread "2") geschieht.
Es ist jedoch durchaus möglich, dass ich den Standard nicht verstanden habe.
Ich stelle fest, dass der für x86_64 generierte Code Folgendes enthält:
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
Und vorausgesetzt, dass R1 und X1 in dieser Reihenfolge auftreten, ergibt dies das erwartete Ergebnis.
Mein Verständnis von x86_64 ist jedoch, dass Lesevorgänge in der Reihenfolge mit anderen Lese- und Schreibvorgängen in der Reihenfolge mit anderen Schreibvorgängen erfolgen, Lese- und Schreibvorgänge jedoch möglicherweise nicht in der richtigen Reihenfolge. Was bedeutet, dass X1 vor R1 und sogar X1, X2, W2, R1 in dieser Reihenfolge auftreten kann - glaube ich. [Dies scheint äußerst unwahrscheinlich, aber wenn R1 durch einige Cache-Probleme aufgehalten würde?]
Bitte: Was verstehe ich nicht?
Ich stelle fest, dass, wenn ich die Ladevorgänge / Speicher von ts->ready
in ändere memory_order_seq_cst
, der für die Speicher generierte Code lautet:
xchg %cl,(%rdi)
Das stimmt mit meinem Verständnis von x86_64 überein und wird das erwartete Ergebnis liefern.
quelle
8.2.3.3 Stores Are Not Reordered With Earlier Loads
. Ihr Compiler übersetzt Ihren Code also korrekt (wie überraschend), sodass Ihr Code effektiv vollständig sequentiell ist und nichts Interessantes gleichzeitig passiert.Antworten:
Das Speichermodell von x86 besteht im Wesentlichen aus sequentieller Konsistenz plus einem Speicherpuffer (mit Speicherweiterleitung). Jeder Store ist also ein Release-Store 1 . Aus diesem Grund benötigen nur seq-cst-Geschäfte spezielle Anweisungen. ( C / C ++ 11-Atomics-Zuordnungen zu asm ). Außerdem enthält https://stackoverflow.com/tags/x86/info einige Links zu x86-Dokumenten, einschließlich einer formalen Beschreibung des x86-TSO-Speichermodells (für die meisten Menschen grundsätzlich nicht lesbar; erfordert das Durchblättern vieler Definitionen).
Da Sie bereits Jeff Preshings ausgezeichnete Artikelserie lesen, verweise ich Sie auf eine andere, die ausführlicher behandelt wird: https://preshing.com/20120930/weak-vs-strong-memory-models/
Die einzige Neuordnung, die auf x86 zulässig ist, ist StoreLoad, nicht LoadStore , wenn wir in diesen Begriffen sprechen. (Die Speicherweiterleitung kann besonders unterhaltsam sein, wenn ein Ladevorgang einen Speicher nur teilweise überlappt. Global Invisible-Anweisungen zum Laden , obwohl dies im vom Compiler generierten Code für nie angezeigt wird
stdatomic
.)@EOF kommentierte mit dem richtigen Zitat aus Intels Handbuch:
Fußnote 1: Ignorieren schwach geordneter NT-Geschäfte; Dies ist der Grund, warum Sie normalerweise
sfence
nach dem Ausführen von NT-Speichern. Bei C11 / C ++ 11-Implementierungen wird davon ausgegangen, dass Sie keine NT-Speicher verwenden. Wenn dies der Fall ist, verwenden Sie_mm_sfence
vor einem Freigabevorgang, um sicherzustellen, dass Ihre NT-Speicher respektiert werden. ( Verwenden Sie_mm_mfence
/_mm_sfence
in anderen Fällen im Allgemeinen nicht / normalerweise müssen Sie nur die Neuordnung zur Kompilierungszeit blockieren. Oder verwenden Sie einfach stdatomic.)quelle