5

我试图让一个类运行一个线程,该线程将在循环中调用一个名为 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
4

2 回答 2

12

在使用完对象之前,您不能让对象超出范围!return 0;结束main原因fn超出范围。因此,当您开始调用tick时,无法保证该对象甚至不再存在。

(其中的逻辑~Runnable完全被破坏了。在析构函数内部为时已晚——对象已经至少部分被破坏了。)

于 2012-05-17T11:15:32.963 回答
6

使用继承作为线程控制的父和子实现函数的方法通常是一个坏主意。这种方法的常见问题来自于构造和破坏:

  • 如果线程是从父(控件)中的构造函数启动的,那么它可能会在构造函数完成之前开始运行,并且线程可能会在完整对象完全构造之前调用虚函数

  • 如果线程在父级的析构函数中停止,那么当控件加入线程时,线程正在对不再存在的对象执行方法。

在您的特定情况下,您遇到的是第二种情况。程序开始执行,并在main第二个线程中启动。此时主线程和新启动的线程之间存在竞争,如果新线程更快(不太可能,因为启动线程是一项昂贵的操作),它将调用Tick将分派给最终覆盖器的成员方法Fn::Tick

但是如果主线程更快,它将退出范围main,并开始销毁对象,它将完成对象的销毁,并在线程Fn的构造过程中。如果主线程足够快,它将到达第二个线程之前,并在那里等待第二个线程调用现在最终的覆盖器,即. 请注意,这是未定义的行为,并且不能保证,因为第二个线程正在访问一个正在被销毁的对象。RunnablejoinjoinTickRunnable::Tick

此外,还有其他可能的顺序,例如,第二个线程可以Fn::Tick在主线程开始销毁之前调度到,但在主线程销毁Fn子对象之前可能无法完成函数,在这种情况下,您的第二个线程将调用死对象的成员函数。

您应该遵循 C++ 标准中的方法:将控制逻辑分离,完全构造将要运行的对象并在构造期间将其传递给线程。请注意,这是 Java 的情况Runnable,建议在扩展Thread类时使用它。请注意,从设计的角度来看,这种分离是有意义的:线程对象管理执行,而可运行对象是要执行的代码。线程不是股票代码,而是控制股票代码执行的东西。在你的代码Runnable中不是可以运行的东西,而是可以运行的东西碰巧从它派生的其他对象。

于 2012-05-17T11:34:51.973 回答