2012-05-17 50 views
4

我試圖讓一個類運行一個線程,它將在循環中調用名爲Tick()的虛擬成員函數。然後我試着派生一個類並覆蓋base :: Tick()。C++ 11線程不能使用虛擬成員函數

但是當執行時,程序只調用基類的Tick而不是覆蓋一個。任何解決方案

#include <iostream> 
#include <atomic> 
#include <thread> 
#include <chrono> 

using namespace std; 

class Runnable { 
public: 
    Runnable() : running_(ATOMIC_VAR_INIT(false)) { 

    } 
    ~Runnable() { 
    if (running_) 
     thread_.join(); 
    } 
    void Stop() { 
    if (std::atomic_exchange(&running_, false)) 
     thread_.join(); 
    } 
    void Start() { 
    if (!std::atomic_exchange(&running_, true)) { 
     thread_ = std::thread(&Runnable::Thread, this); 
    } 
    } 
    virtual void Tick() { 
    cout << "parent" << endl; 
    }; 
    std::atomic<bool> running_; 

private: 
    std::thread thread_; 
    static void Thread(Runnable *self) { 
    while(self->running_) { 
     self->Tick(); 
     std::this_thread::sleep_for(std::chrono::milliseconds(100)); 
    } 
    } 
}; 

class Fn : public Runnable { 
public: 
    void Tick() { 
    cout << "children" << endl; 
    } 
}; 

int main (int argc, char const* argv[]) 
{ 
    Fn fn; 
    fn.Start(); 
    return 0; 
} 

輸出:

parent 
+7

就我個人而言,我認爲解決這個問題並不是最好的解決方案。我認爲最好的解決方案是首先不要造成這個問題。對於非問題來說,這是一個糟糕的解決方案。 –

+8

不要用C++編寫Java。 –

+0

你說得對。我爲捕捉系統做了這個工作,它會以恆定的頻率查詢一些源,例如10次/秒,並將數據提取到隊列中。並且有一個監視器將以另一個恆定頻率從隊列中拉出並顯示數據。因此,這是我想出的解決方案,創建一個基類,然後使用向量/列表來保存所有的源和監視器(而源可以在運行時添加/刪除)。有更好的解決方案 – xiaoyi

回答

11

你不能讓一個對象用完的範圍,直到你使用它完成! main末尾的return 0;導致fn超出範圍。因此,當你開始呼叫tick時,不能保證對象甚至存在。

(在~Runnable邏輯完全破碎裏面的析構函數是太晚 - 對象已至少部分地被破壞。)

+0

什麼對象不存在了?析構函數加入線程。如果線程正在運行,則該對象存在。 –

+4

@ R.MartinhoFernandes:* parent *的析構函數加入該線程,但那時該對象不再是一個孩子。 –

+0

** Runnable **析構函數加入該線程。這太遲了,因爲線程正在運行的實際對象('fn'對象)已被銷燬。 (回答更新。) –

3

使用繼承與作爲控制父的方法線程和子程序實現這些函數通常是個壞主意。這種方法的常見問題來自於建築和破壞:

  • 如果線程是從父(控制)的構造開始那麼它可能會開始構造完成,該線程可能會調用虛函數之前運行在完整對象已完全構建之前

  • 如果線程在父級的析構函數中停止,那麼在控制加入線程時,線程正在對不再存在的對象執行方法。

在你的具體情況下,你正在打第二個案件。程序開始執行,在main中啓動第二個線程。此時,主線程和新啓動的線程之間存在競爭,如果新線程更快(不太可能,因爲啓動線程是一項昂貴的操作),它將調用成員方法Tick,該方法將被分派到最終的overrider Fn::Tick

但是,如果主線程是快將退出的main範圍,它將啓動對象的破壞,它會完成Fn對象的破壞和建設Runnable的過程中它會join線程。如果主線程足夠快,它將在第二個線程之前將其設置爲join,並等待第二個線程在現在上調用Tick最終覆蓋程序即Runnable::Tick。請注意,這是未定義的行爲,並且不能保證,因爲第二個線程正在訪問正在銷燬的對象。

此外,還有其他可能的順序,例如像第二個線程可分派到Fn::Tick主線程開始前摧毀,但可能無法完成的功能主線程銷燬Fn子對象之前,在這種情況下,你的第二個線程會調用死對象上的成員函數。

你應該而按照C++標準的做法:控制邏輯分開,完全構建將要運行的對象,並在施工期間將它傳遞給該線程。請注意,這是Java的Runnable的情況,建議在擴展Thread類時使用。請注意,從設計角度來看,這種分離是有意義的:線程對象管理執行,並且可運行是要執行的代碼。 線程不是股票代碼,而是什麼控制股票代碼的執行。並且在您的代碼中Runnable不是可以運行的東西,而是運行的其他對象碰巧發生的其他對象。