[英]C++11 thread doesn't work with virtual member function
我正在嘗試讓類運行一個線程,它將在循環中調用名為Tick()的虛擬成員函數。 然后我嘗試派生一個類並覆蓋base :: Tick()。
但是在執行時,程序只調用基類的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
在完成使用之前,您不能讓對象超出范圍! return 0;
在main
結束時fn
超出范圍。 因此,當你開始調用tick
,不能保證對象甚至不再存在。
( ~Runnable
的邏輯完全被破壞。析構函數內部太遲了 - 對象已經至少部分被破壞了。)
使用繼承與父作為線程的控件和實現函數的子項的方法通常是一個壞主意。 這種方法的常見問題來自構建和破壞:
如果線程是從父(控件)中的構造函數啟動的,則它可能在構造函數完成之前開始運行,並且線程可能在完整構造完整對象之前調用虛函數
如果線程在父類的析構函數中停止,那么在控件加入線程時,線程正在對不再存在的對象執行方法。
在你的特殊情況下,你正在遇到第二種情況。 程序開始執行,並在main
中啟動第二個線程。 此時主線程和新啟動之間存在競爭,如果新線程更快(不太可能,因為啟動線程是一項昂貴的操作),它將調用將被調度到最終覆蓋的成員方法Tick
Fn::Tick
。
但是如果主線程更快,它將退出main
的范圍,並且它將開始破壞對象,它將完成對Fn
對象的破壞,並且在構造Runnable
期間它將join
線程。 如果主線程足夠快,它將在第二個線程之前進入join
,並在那里等待第二個線程在現在最終的覆蓋器上運行Tick
,這個覆蓋器是Runnable::Tick
。 請注意,這是未定義的行為 ,並且無法保證,因為第二個線程正在訪問正在銷毀的對象。
此外,還有其他可能的排序,例如,第二個線程可以在主線程開始銷毀之前調度到Fn::Tick
,但可能在主線程銷毀Fn
子對象之前沒有完成該函數,在這種情況下你的第二個線程將在死對象上調用成員函數。
您應該遵循C ++標准中的方法:將控件與邏輯分開,完全構造將運行的對象並在構造期間將其傳遞給線程。 請注意,這是Java的Runnable
的情況,建議使用它來擴展Thread
類。 請注意,從設計的角度來看,這種分離是有意義的: 線程對象管理執行,而runnable是要執行的代碼。 線程不是股票代碼 ,而是控制股票代碼執行的內容。 在你的代碼中, Runnable
不是可以運行的東西,而是運行其他碰巧從它派生的對象。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.