Acquire-Release-Semantik - Das typische Missverständnis

Acquire-Release-Semantik - Das typische Missverständnis

Eine Freigabeoperation synchronisiert sich mit einer Erfassungsoperation auf derselben atomaren Variablen. So können wir problemlos Threads synchronisieren, wenn ... . Im heutigen Beitrag geht es um das wenn .

Was ist meine Motivation, einen Beitrag über das typische Missverständnis der Erwerb-Freigabe-Semantik zu schreiben? Sicher, ich und viele meiner Zuhörer und Auszubildenden sind schon in die Falle getappt. Aber erstmal der einfache Fall.

Warten inbegriffen

Ich verwende dieses einfache Programm als Ausgangspunkt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// acquireReleaseWithWaiting.cpp

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

std::vector<int> mySharedWork;
std::atomic<bool> dataProduced(false);

void dataProducer(){
 mySharedWork={1,0,3};
 dataProduced.store(true, std::memory_order_release);
}

void dataConsumer(){
 while( !dataProduced.load(std::memory_order_acquire) );
 mySharedWork[1]= 2;
}

int main(){
 
 std::cout << std::endl;

 std::thread t1(dataConsumer);
 std::thread t2(dataProducer);

 t1.join();
 t2.join();
 
 for (auto v: mySharedWork){
 std::cout << v << " ";
 }
 
 std::cout << "\n\n";
 
}

Der Consumer-Thread t1 in Zeile 17 wartet, bis der Consumer-Thread t2 in Zeile 13 dataProduced auf true gesetzt hat. dataPruduced ist der Wächter, denn er garantiert, dass der Zugriff auf die nicht-atomare Variable mySharedWork synchronisiert wird. Das heißt, zuerst initialisiert der Producer-Thread t2 mySharedWork, dann beendet der Consumer-Thread t2 die Arbeit, indem er mySharedWork[1] auf 2 setzt. Das Programm ist also wohldefiniert.

Die Grafik zeigt das Vorher Beziehung innerhalb der Threads und synchronisiert-mit Beziehung zwischen den Fäden. synchronisieren-mit stellt ein passiert-vorher her Beziehung. Der Rest der Argumentation ist die Transitivität des Vorherigen Beziehung. mySharedWork={1,0,3} passiert-vorher mySharedWork[1]=2.

Doch welcher Aspekt fehlt bei dieser Argumentation oft. Das wenn.

Wenn, ...

Was passiert, wenn der Consumer-Thread t2 in Zeile 17 wartet nicht auf den Producer-Thread?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// acquireReleaseWithoutWaiting.cpp

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

std::vector<int> mySharedWork;
std::atomic<bool> dataProduced(false);

void dataProducer(){
 mySharedWork={1,0,3};
 dataProduced.store(true, std::memory_order_release);
}

void dataConsumer(){
 dataProduced.load(std::memory_order_acquire);
 mySharedWork[1]= 2;
}

int main(){
 
 std::cout << std::endl;

 std::thread t1(dataConsumer);
 std::thread t2(dataProducer);

 t1.join();
 t2.join();
 
 for (auto v: mySharedWork){
 std::cout << v << " ";
 }
 
 std::cout << "\n\n";
 
}

Das Programm hat ein undefiniertes Verhalten, weil es einen Datenwettlauf auf der Variablen mySharedWork gibt. Falls ich das Programm laufen lasse, wird das undefinierte Verhalten sofort sichtbar. Das gilt für Linux und Windows.

Was ist das Problem? Es enthält:store(true, std::memory_order_release) synchron izes-mit dataProduced.load(std::memory_order_acquire). Ja, natürlich, aber das bedeutet nicht, dass die Erfassungsoperation auf die Freigabeoperation wartet. Genau das zeigt die Grafik. In der Grafik wird die Anweisung dataProduced.load(std::memory_order_acquire) vor der Anweisung dataProduced.store(true, std::memory_order_release) ausgeführt. Wir haben also kein synchronisieren-mit Beziehung.

Die Lösung

synchronisieren mit bedeutet in diesem speziellen Fall:If dataProduced.store(true, std::memory_order_release) geschieht vor dataProduced.load(std::memory_order_acquire), dann Alle sichtbaren Auswirkungen von Operationen vor dataProduced.store(true, std::memory_order_release) sind nach dataProduced.load(std::memory_order_acquire) sichtbar. Der Schlüssel ist das Wort wenn. Genau das wenn wird im ersten Programm mit (while(!dataProduced.load(std::memory_order_acquire))) garantiert.

Noch einmal, aber formell.

  • Alle Operationen vor dataProduced.store(true, std::memory_order_release)passiert-before alle Operationen nach dataProduced.load(std::memory_order_acquire), falls gilt:dataProduced.store(true, std::memory_order_release) passiert-before dataProduced.load(std::memory_order_acquire).

Was kommt als nächstes?

Acquire-Release-Semantik mit Operationen auf atomaren Variablen. Funktioniert das? Ja, mit Zäunen. Schauen Sie sich den nächsten Beitrag an.