2016-12-09 77 views
7

在下面的節目,我有一個虛擬的呼叫從一個線程中:虛擬呼叫忽略派生

#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(); 
} 

我希望該程序將打印:

I am C. 
I am C. 

instead it prints

I am C. 
I am A. 

在程序中,我等到派生對象完全構造,然後再調用t他虛擬的成員函數。但是線程在對象完全構建之前啓動。

如何保證虛擬電話?

+0

你用什麼平臺? VS2015U3 - 「我是C」是預期的兩倍。 –

+0

在問題中的鏈接中使用了clang,但我[與gcc相同](http://coliru.stacked-crooked.com/a/688e7d3c5107e57a)。同樣,使用'Microsoft Visual Studio Community 2015版本14.0.25431.01 Update 3',我也可以得到同樣的結果。但是如果我在VS2015的調試模式下單步執行,那麼我會得到兩次「我是C」。 – wally

+0

嗯,這是奇怪的。我有'Microsoft Visual Studio Professional 2015版本14.0.25431.01 Update 3'。適用於調試和發佈。 –

回答

8

顯示的代碼顯示競爭條件和未定義的行爲。

在你的main():

C c; 

// ... 

a->set_ready(); 

,立即後set_ready()回報,執行線程葉main()。這導致立即破壞c,從超類C開始,並且繼續銷燬B,然後A

c在自動範圍內聲明。這意味着只要main()返回,它就消失了。加入了無形的合唱團。沒有更多。它不復存在。這是一個前客體。

join()是父類的析構函數。沒有什麼東西可以阻止C被銷燬。析構函數只會暫停並等待加入線程,當超類被破壞,但C將立即開始銷燬!

一旦C超類被銷燬,其虛方法不再存在,調用虛函數將最終執行基類中的虛函數。

同時另一個執行線程正在等待互斥量和條件變量。競爭條件是,您不能保證另一個執行線程將在父線程銷燬C之前被喚醒並開始執行,它在發送條件變量之後立即執行。

所有表示條件變量的信息都是,無論執行線程在條件變量上旋轉,該執行線程都將開始執行。最終。該線程可以在一個非常負載的服務器上,在通過條件變量發送信號之後的幾秒鐘後開始執行。它的對象很久以前就走了。它在自動範圍內,並且main()將其銷燬(或者,C子類已經被銷燬,並且A的析構函數正在等待加入該線程)。

你所觀察的行爲是父線程管理摧毀C超的std::thread都繞來製作它的虛擬方法調用,從條件變量接收到信號後,和解鎖的互斥前。

這就是競爭條件。

此外,在虛擬對象被銷燬的同時執行虛擬方法調用已經是非啓動器。這是未定義的行爲。即使執行線程在重寫的方法中結束,它的對象也會被另一個線程同時銷燬。無論你轉向哪個方向,你都很容易搞砸。

經驗教訓:搭建std::thread執行this對象是未定義行爲的雷區。有辦法正確地做,但很難。

+0

優秀的分析。儘管如此,我仍然無法理解,當競爭條件發生時,子線程結束執行父方法。好的,這是UB,但是很奇怪。 –

+1

@ A.S.H - 其實很簡單。當'C'的析構函數已經完成時會發生這種情況。當一個子類被破壞時,任何重寫的方法都會回到超類。在超類的析構函數中調用虛擬方法是非常好的,它最終會在基類中執行非重載方法。這就是本質上發生的事情(除了銷燬和執行發生在不同的線程)。 –

+0

是的,我現在看到。豎起大拇指。 –

2

這是事件的最可能的序列:

  • 的對象的一部分構成,其開始
  • 的對象的B部被構造一個線程。
  • 構造對象的C部分。
  • getname在主線程上被調用,它打印出「我是C!」因爲它是C.
  • 主線程通知其他線程(我稱之爲打印線程)
  • main開始返回。
  • 對象的C部分被破壞。
  • 對象的B部分被破壞。
  • 對象的A部分被破壞......但是這會阻塞,直到打印線程退出。
  • 既然主線程被阻塞,操作系統將切換到打印線程。
  • 打印線程調用getname,打印出「我是A!」因爲它是一個A(現在對象的C和B部分已經被銷燬)。
  • 印刷線程退出
  • 主線程喚醒,完成銷燬A部分並退出程序。

要獲得預期的行爲可靠,你需要等待打印線程閉幕main}之前退出

+0

感謝您的回答。我現在會試着想出一些能使'C'保持活力的東西,直到'A'能夠收穫其器官呃,我的意思是完成虛擬呼叫。 – wally

+1

優秀的答案。我認爲這兩個答案都會爲比賽條件喚起相同的順序。 –

0

其他答案是確定的,但不顯示可能的修復方法。下面是一個額外的變量,並等待同一個程序:

#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.