Hallo,
der folgende Code liefert ein Data Race. Das ist kein produktiver Code,
sondern ein MCE, dass bei mir den Fehler reproduzierbar erzeugt. Die
Klasse miniClass stellt eine Klasse aus einem Framework nach.
Der Code benutzt die single producer single consumer queue von Cameron
alias "Moody Camel", https://github.com/cameron314/readerwriterqueue.
Sieht vielleicht jemand, wo das Problem liegt? Ich würde mal vermuten,
dass das Problem in meinem Code liegt und nicht in Camerons, da seine
queue doch recht verbreitet ist und so ein einfacher Fall sicher schon
mal aufgestoßen wäre?
Das Data Race tritt im operator= in der ersten Zeile auf. Wenn man den
Operator entfernt klappt alles.
Dass das ein Data Race vorliegt, ist eine Meldung von ThreadSanitizer.
Herzlichen Dank für jeden Hinweis
Timm
Sleep ist kein adäquates Mittel um Threads zu synchronisieren! Wenn du
statt try_dequeue wait_dequeue verwenden würdest, würde die Queue auf
ihrere interne Semaphore warten und nicht sofort aufgeben, wenn die
Queue noch leer ist.
Hallo jemand,
vielen Dank! Deine Diagnose erscheint zutreffend. Sehr spannende (für
mich) Lektion.
Könntest Du (oder jemand anderes, der es weiß) mir vielleicht etwas im
Detail erklären, was da vor sich geht? Fürs Verständnis?
1. Warum reicht 1s nicht aus, dass die queue mit schreiben fertig wird?
Du hast ja offensichtlich recht, aber warum ist das so?
2. Das Data Race tritt auf, weil die Queue genau in dem Moment mit
schreiben fertig wird, wie mit lesen? Die Queue ist zwar entsprechend
abgeschirmt, aber mein Klassenobjekt nicht, weswegen ich dann ein Data
Race im operator= habe, korrekt?
3. wieso tritt mit default operator= der Effekt nicht auf?
Vielen Dank
Timm
Ganz konkret ist das Problem, dass man bei Threads grundsätzlich
annehmen muss, dass diese an einer beliebigen Stelle für unbestimmte
Zeit blockiert sein können, z. B.
1
std::thread t1([] {
2
// Thread darf genau hier für unbestimmte Zeit stecken bleiben!
3
miniClass c1,c2;
4
q.enqueue(c1);
5
q.enqueue(c2);
6
});
7
8
std::thread t2([] { // währenddessen kann dieser Thread komplett ausgeführt werden
9
miniClass c3,c4;
10
c3 = dequeue(); // diese Funktion würde dann kein Ergebnis bringen
11
c4 = dequeue();
12
});
Die Ergebnisse in t2 hängen also davon ab, ob die Funktionen in t1 vor,
danach oder gar gleichzeitig ausgeführt werden. Die Function
wait_dequeue synchronisiert diesen Ablauf, es wird erst weitergemacht,
wenn die Queue tatsächlich aufgefüllt wurde. t2 kann dann also gar
nicht vor t1 fertig werden.
Sleep reicht eben nicht aus, weil t1 durchaus länger brauchen darf, als
die Sleep-Dauer. Mit Semaphore, Mutex und co., wie es die Bibliothek
intern verwendet, wird das Problem deterministisch gelöst.
Die Warnung vom ThreadSanitizer bekomme ich übrigens auch, wenn ich den
operator= auskommentiere, aber nur bei g++ mit -O0 oder -O1, mit clang++
gar nicht. Das Ding ist also keine Wunderwaffe.
Hallo,
@jemand
wie gesagt, vielen Dank für Deine Mühe! Sehr hilfreich!
Ich hätte da noch eine Nachfrage:
Das Problem ist ja nicht, dass das try_decueue ein leeres miniClass
zurückliefert, sondern, dass das Schicksal beschließt unabhängig von
verstrichener Zeit, das miniClass Objekt erst in dem Moment fertig zu
stellen, in dem ich es auch auslesen will.
Was dann zum DataRace im operator= führt.
Man könnte also doch sagen, dass mit miniClass Objekten ein korrekter
Betrieb dieser lockfreien queue nicht möglich ist. Was irgendwo auch gar
nicht so unlogisch ist, denn wenn die queue warten müsste, bis der
Konstruktor fertig ist, wäre sie ja eben nicht mehr lock-free.
Kann man also sagen, dass eine lock-free queue nur mit pod's geht? Gibt
es ein anderes Kriterium, dass man da ansetzen kann?
Vielen Dank
Timm
Ich sehe keinen Grund, warum sich deine miniClass anders verhalten
sollte als gewöhnliche POD Konstrukte, sie modifiziert ja nur ihren
eigenen Zustand ohne Seiteneffekte.
Lock-Free bedeutet bei dieser Queue nur, dass enqueue und dequeue den
internen Speicher oder Zustand nicht sperren müssen und die korrekte
Funktion durch (einfache) atomare Operationen sichergestellt ist,
dadurch gibt es aber die Enschränkung, dass es nur ein Enqueue-Thread
und ein Dequeue-Thread zu einem gegebenen Zeitpunkt geben darf.
Timm R. schrieb:> das Schicksal beschließt unabhängig von> verstrichener Zeit, das miniClass Objekt erst in dem Moment fertig zu> stellen, in dem ich es auch auslesen will.
Was meinst du mit fertigstellen? Der Zugriff auf ein Element in der
Queue kann erst stattfinden, nachdem es konstruiert wurde, dafür sorgt
die Queue.
Wenn du dich auf die Warnung des ThreadSanitizers beziehst: Die ist
weitgehend unabhängig von irgendwelchen Timings, es müssen keine
tatsächlichen Effekte eingetreten sein.
Jemand schrieb:> Was meinst du mit fertigstellen? Der Zugriff auf ein Element in der> Queue kann erst stattfinden, nachdem es konstruiert wurde, dafür sorgt> die Queue.
hmm, aber die Race Condition tritt doch auf, weil ich mit dem
Lesezugriff für das dequeue den Schreibzugriff für das enqueue treffe?