C++ / Data Race

OP (Firma: privatfrickler.de) #5864381
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
1
#include "atomicops.h"
2
#include "readerwriterqueue.h"
3
#include <thread>
4
#include <chrono>
5
//==============================================================================
6

7
struct miniClass {
8
    miniClass () {}
9
    
10
    miniClass (const miniClass& other) {
11
        packedData.allocatedData = other.packedData.allocatedData;
12
    }
13
    
14
    miniClass& operator= (miniClass&& other) noexcept {
15
        packedData.allocatedData = other.packedData.allocatedData;
16
        return *this;
17
    }
18
    
19
    union PackedData {
20
        uint8_t* allocatedData;
21
        uint8_t asBytes[sizeof (uint8_t*)];
22
    };
23
    PackedData packedData;
24
};
25

26
moodycamel::ReaderWriterQueue<miniClass> q;
27

28
miniClass dequeue() {
29
    miniClass c;
30
    q.try_dequeue(c);
31
    return c;
32
}
33

34
int main (int argc, char* argv[])
35
{
36
    std::thread t1([] {
37
        miniClass c1,c2;
38
        q.enqueue(c1);
39
        q.enqueue(c2);
40
    });
41
    
42
    std::thread t2([] {
43
        miniClass c3,c4;
44
        c3 = dequeue();
45
        c4 = dequeue();
46
    });
47
    
48
    t1.join();
49
    std::this_thread::sleep_for (std::chrono::seconds(1));
50
    t2.join();
51
    
52
    return 0;
53
}
Gast
t2 sieht die Queue entweder als
a) mindestens teilweise gefüllt
oder
b) leer.

Was soll überhaupt
1
std::this_thread::sleep_for (std::chrono::seconds(1));
bezwecken?
OP (Firma: privatfrickler.de)
Hallo,

Jemand schrieb:
> Was soll überhaupt
>
1
std::this_thread::sleep_for (std::chrono::seconds(1));
> bezwecken?

äh, ja, sorry. Die main muss so aussehen, Fehler bleibt der selbe.

danke

 Timm
1
int main (int argc, char* argv[])
2
{
3
    std::thread t1([] {
4
        miniClass c1,c2;
5
        q.enqueue(c1);
6
        q.enqueue(c2);
7
    });
8
    
9
    std::this_thread::sleep_for (std::chrono::seconds(1));
10
    
11
    std::thread t2([] {
12
        miniClass c3,c4;
13
        //c3 = dequeue();
14
        //c4 = dequeue();
15
        dequeue2(c3);
16
        dequeue2(c4);
17
    });
18
    
19
    t1.join();
20
    t2.join();
21
    
22
    return 0;
23
}
Gast
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.
OP (Firma: privatfrickler.de)
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
Gast
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.
Gast
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.
OP (Firma: privatfrickler.de)
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
Gast
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.
Gast
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.
OP (Firma: privatfrickler.de)
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?

Antwort schreiben

Eine eigene Frage? Dieser Thread ist über 7 Jahre alt. Stell sie in einem eigenen Thread – dort lesen mehr Leute mit, und sie wird öfter und schneller beantwortet.

Neuen Thread starten

Eine Lösung, Ergänzung oder Rückmeldung zu diesem Thread? Dann antworte hier.

Formatierung
  • [c]C-Code[/c]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] – Link zu einem Artikel
  • Verweis auf einen anderen Beitrag: Rechtsklick auf dessen Beitrags-ID (#8078628), „Adresse kopieren“, in den Text einfügen

Mehr zur Formatierung

Längeren Sourcecode als Anhang, nicht in den Text. JPG nur für Fotos und Scans; Zeichnungen und Screenshots als PNG oder GIF. Siehe Bildformate.

Schon registriert? Anmelden

Wird nicht angezeigt. Wir schicken dir einen Link; sobald du damit bestätigst, erscheint dein Beitrag.

Mit dem Abschicken legst du einen kostenlosen Account an und erkennst die Nutzungsbedingungen und die Datenschutzerklärung an.