2012-05-17 51 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不是可以运行的东西,而是运行的其他对象碰巧发生的其他对象。