Forum: PC-Programmierung C++ / Data Race


von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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
}

von Jemand (Gast)


Lesenswert?

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?

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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
}

: Bearbeitet durch User
von Jemand (Gast)


Lesenswert?

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.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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

von Jemand (Gast)


Lesenswert?

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.

von Jemand (Gast)


Lesenswert?

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.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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

von Jemand (Gast)


Lesenswert?

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.

von Jemand (Gast)


Lesenswert?

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.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.