2016-12-09 3 views
7

Im folgenden Programm, das ich aus einem Thread einen virtuellen Aufruf haben:Virtuelles Call innerhalb Thread abgeleitet ignoriert Klasse

#include <iostream> 
#include <string> 
#include <thread> 
#include <mutex> 
#include <condition_variable> 

class A { 
public: 
    virtual ~A() { t.join(); } 
    virtual void getname() { std::cout << "I am A.\n"; } 
    void printname() 
    { 
     std::unique_lock<std::mutex> lock{mtx}; 
     cv.wait(lock, [this]() {return ready_to_print; }); 
     getname(); 
    }; 
    void set_ready() { std::lock_guard<std::mutex> lock{mtx}; ready_to_print = true; cv.notify_one(); } 
    void go() { t = std::thread{&A::printname,this}; }; 

    bool ready_to_print{false}; 
    std::condition_variable cv; 
    std::mutex mtx; 
    std::thread t{&A::printname,this}; 
}; 

class B : public A { 
public: 
    int x{4}; 
}; 

class C : public B { 
    void getname() override { std::cout << "I am C.\n"; } 
}; 

int main() 
{ 
    C c; 
    A* a{&c}; 
    a->getname(); 
    a->set_ready(); 
} 

ich das Programm hatte gehofft, würde drucken:

I am C. 
I am C. 

Aber instead it prints:

I am C. 
I am A. 

Im Programm warte ich, bis das abgeleitete Objekt vollständig aufgebaut ist, bevor ich t anrufe Die virtuelle Mitgliedsfunktion. Der Thread wird jedoch gestartet, bevor das Objekt vollständig erstellt wurde.

Wie kann der virtuelle Anruf sichergestellt werden?

+0

Welche Plattform benutzen Sie? VS2015U3 - "Ich bin C" zweimal wie erwartet. –

+0

In dem Link in der Frage verwendet es clang, aber ich [das gleiche mit gcc] (http://coliru.stacked-crooked.com/a/688e7d3c5107e57a). Auch mit 'Microsoft Visual Studio Community 2015 Version 14.0.25431.01 Update 3' bekomme ich das gleiche. Wenn ich jedoch in VS2015 im Debug-Modus durchtrete, bekomme ich zweimal "Ich bin C". – wally

+0

hmm das ist seltsam. Ich habe 'Microsoft Visual Studio Professional 2015 Version 14.0.25431.01 Update 3'.Funktioniert gut für beide - Debug und Release. –

Antwort

8

Der angezeigte Code weist eine Racebedingung und ein undefiniertes Verhalten auf.

In Ihrer main():

C c; 

// ... 

a->set_ready(); 

Unmittelbar nach set_ready() zurückkehrt, verlässt Ausführungs-Thread main(). Dies führt sofortige Zerstörung von c, beginnend mit der Oberklasse C, und weiterhin zu zerstören B, dann A.

c wird im automatischen Bereich deklariert. Das heißt, sobald main() zurückkommt, ist es weg. Vereinte den Chor unsichtbar. Es ist nicht mehr. Es hörte auf zu existieren. Es ist ein Ex-Objekt.

Ihre join() ist in der Destruktor der übergeordneten Klasse. Nichts hält C davon ab, zerstört zu werden. Der Destruktor wird nur pausieren und warten, um dem Thread beizutreten, wenn die Superklasse zerstört wird, aber C wird sofort zerstört werden!

Sobald die Superklasse C zerstört ist, existiert ihre virtuelle Methode nicht mehr, und das Aufrufen der virtuellen Funktion endet mit der Ausführung der virtuellen Funktion in der Basisklasse.

Der andere Ausführungsthread wartet auf den Mutex und die Zustandsvariable. Die Race-Bedingung ist, dass Sie keine Garantie haben, dass der andere Ausführungsthread aufwacht und die Ausführung beginnt, bevor der übergeordnete Thread C zerstört, was unmittelbar nach der Signalisierung der Zustandsvariablen erfolgt.

Alles, was die Zustandsvariable signalisiert, besteht darin, dass der Ausführungsthread, der sich auf der Bedingungsvariablen dreht, mit der Ausführung beginnt. Schließlich. Dieser Thread könnte, auf einem sehr geladenen Server, Sekunden später starten, nachdem er über die Zustandsvariable signalisiert wurde. Sein Objekt ist vor langer Zeit verschwunden. Es war im automatischen Bereich, und main() zerstört es (oder vielmehr ist die C Unterklasse bereits zerstört, und A 's Destruktor wartet auf den Thread beizutreten).

Das Verhalten, das Sie beobachten, ist das übergeordnete Thread, das die C Superklasse zerstört, bevor std::thread seinen virtuellen Methodenaufruf durchführt, nachdem er das Signal von der Zustandsvariablen empfangen und seinen Mutex entsperrt hat.

Das ist die Race-Bedingung.

Außerdem ist das Ausführen eines virtuellen Methodenaufrufs zur gleichen Zeit, zu der das virtuelle Objekt zerstört wird, bereits ein Nicht-Starter. Es ist undefiniertes Verhalten. Selbst wenn der Ausführungsthread in der überschriebenen Methode endet, wird sein Objekt gleichzeitig von einem anderen Thread zerstört. An diesem Punkt bist du ziemlich verrückt, egal auf welche Art du dich drehst.

Lektionen lernen: Rigging ein std::thread zur Ausführung etwas in this Objekt ist ein Minenfeld von undefiniertem Verhalten. Es gibt Möglichkeiten, es richtig zu machen, aber es ist schwer.

+0

Ausgezeichnete Analyse. Dennoch bleibe ich irgendwie perplex, dass, wenn die Race-Bedingung auftritt, der Child-Thread die Elternmethode ausführt. Ok, es ist UB, aber ein seltsames. –

+1

@ A.S.H - Es ist eigentlich ziemlich einfach. Dies geschieht, wenn der Destruktor von "C" bereits beendet ist. Wenn eine Unterklasse zerstört wird, werden alle überschriebenen Methoden auf die Oberklasse zurückgesetzt. Das Aufrufen von virtuellen Methoden im Destruktor der Superklasse ist vollkommen in Ordnung, und es führt schließlich die Ausführung der nicht überschriebenen Methoden in der Basisklasse aus. Das ist im Wesentlichen hier passiert (außer der Zerstörung und der Ausführung in verschiedenen Threads). –

+0

Ja, ich sehe jetzt. Daumen hoch. –

2

Dies ist die wahrscheinlichste Abfolge von Ereignissen:

  • Die Ein Teil des Objekts ausgebildet ist, das ein Gewinde beginnt
  • Der B-Teil des Objekts ausgebildet ist.
  • Der C-Teil des Objekts wird konstruiert.
  • getname wird am Haupt-Thread aufgerufen, der "Ich bin C!" weil es eine C.
  • Der Haupt-Thread den anderen Thread benachrichtigt ist (Ich werde es der Druck Thread nennen)
  • main beginnt zurückzukehren.
  • Der C-Teil des Objekts ist zerstört.
  • Der B-Teil des Objekts ist zerstört.
  • Der A-Teil des Objekts wird zerstört ... aber dies blockiert, bis der Druck-Thread austritt.
  • Jetzt, da der Haupt-Thread blockiert ist, wechselt das Betriebssystem zum Druck-Thread.
  • Der Druck-Thread ruft getname, die "Ich bin A!" weil es ein A ist (wobei die C- und B-Teile des Objekts jetzt zerstört wurden).
  • Der Druck-Thread beendet
  • Der Haupt-Thread wacht auf, beendet die Zerstörung des A-Teils und beendet das Programm.

Um das erwartete Verhalten zuverlässig zu erhalten, müssen Sie den Druck Thread warten vor die Schließung } von main zu verlassen.

+0

Danke für diese Antwort. Ich werde jetzt versuchen, etwas zu finden, das 'C' am Leben hält, bis' A' seine Organe ernten kann, ich meine, den virtuellen Anruf vervollständigen. – wally

+1

Ausgezeichnete Antwort auch. Ich denke, beide Antworten evozieren die gleiche Sequenz für die Race Condition. –

0

Die anderen Antworten sind definitiv, zeigen aber keine mögliche Lösung. Hier ist das gleiche Programm mit zusätzlichen Variablen und Warte:

#include <iostream> 
#include <string> 
#include <thread> 
#include <mutex> 
#include <condition_variable> 

class A { 
public: 
    virtual ~A() { t.join(); } 
    virtual void getname() { std::cout << "I am A.\n"; } 
    void printname() 
    { 
     std::unique_lock<std::mutex> lock{mtx}; 
     cv.wait(lock, [this]() {return ready_to_print; }); 
     getname(); 
     printing_done = true; 
     cv.notify_one(); 
    }; 
    void set_ready() { std::lock_guard<std::mutex> lock{mtx}; ready_to_print = true; cv.notify_one(); } 
    void go() { t = std::thread{&A::printname,this}; }; 

    bool ready_to_print{false}; 
    bool printing_done{false}; 
    std::condition_variable cv; 
    std::mutex mtx; 
    std::thread t{&A::printname,this}; 
}; 

class B : public A { 
public: 
    int x{4}; 
}; 

class C : public B { 
public: 
    ~C() 
    { 
     std::unique_lock<std::mutex> lock{mtx}; 
     cv.wait(lock, [this]() {return printing_done; }); 
    } 
    void getname() override { std::cout << "I am C.\n"; } 
}; 

int main() 
{ 
    C c; 
    A* a{&c}; 
    a->getname(); 
    a->set_ready(); 
} 

Prints:

I am C. 
I am C. 
Verwandte Themen