我正在为std :: thread编写一个OO包装器。代码的简化版本如下所示。这个类的问题是,如果它被立即销毁,它可能会抛出一个错误,因为在类被销毁的同时从线程调用doWork(调用纯虚方法)。
测试用例显示在底部。
如何让这堂课更安全?如果MyConThread具有从MyConThread :: doWork使用的成员变量,那么更复杂的例子会更糟。
我意识到在启动时我有一个类似的问题,在构造派生类之前可以调用doWork。
#include <thread>
class ConThread {
public:
ConThread ()
:t_ (doWorkInternal, this)
{}
~ConThread ()
{
if (t_.joinable()) {
t_.join();//avoid a crash because std::thread will terminate the app if the thread is still running in it's destructor
}
}
std::thread& get () {return t_;};
protected:
virtual void doWork ()=0;
private:
static void doWorkInternal (ConThread* t)
{
try {
t->doWork ();
} catch (...)
{};
}
std::thread t_;
};
我遇到的问题是下面的测试用例:
class MyConThread: public ConThread
{
public:
long i=0;
protected:
void doWork () override
{
for (long j=0; j<1000000_ && requestedToTerminate_==false; j++)
{
++i;
}
}
};
TEST(MyConThreadTest, TestThatCanBeDestroyed)
{
MyConThread mct (); //<== crashes when being destroyed because thread calls t->doWork ()
}
答案 0 :(得分:3)
首先,无论线程对象是否被销毁,程序都会崩溃。它很容易检查,只需在创建对象后插入一些延迟:
using namespace std::chrono_literals;
TEST(MyConThreadTest, TestThatCanBeDestroyed)
{
MyConThread mct ();
std::this_thread::sleep_for(100s);
}
崩溃的发生是因为你从构造函数中调用了一个虚拟方法,这通常是非常糟糕的主意。基本上,在C ++中,对象是按照从base到derived的顺序创建的,当你在ctor中调用纯虚方法时,仍然无法处理重载(因为派生尚未构造)。请参阅此answer。
所以,第一条规则:不要从构造函数或析构函数中调用虚方法(无论是纯粹的还是定义的)。
我认为解决这个问题的最简单方法是添加实际启动线程的start
方法。像这样:
ConThread()
{
}
void start()
{
t_ = std::thread(doWorkInternal, this);
}
通常,我不喜欢将逻辑和线程对象混合在一起的想法,因为这样做会违反单一责任原则。你的对象做两件事 - 它是一个线程,它也有你自己的逻辑。通常最好单独处理这些,这就是为什么std::thread
提供了通过构造将“逻辑”传递给它的方法,并且它不是为用作基类而设计的。我找到了一个关于此的好article,它是关于Qt线程而不是std
线程,但概念是相同的。
我通常在我的代码中做的事情(这也不理想,但更干净):
std::thread readerThread([]
{
DatasetReader reader;
reader.init();
reader.run();
});
std::thread mesherThread([]
{
Mesher mesher;
mesher.init();
mesher.run();
});
readerThread.join();
mesherThread.join();
如果你想在dtor中自动加入你的线程,只需在std::thread
周围创建一个包装器,但保留用于将逻辑传递给它的接口(如lambda,或函数指针和参数等)。
答案 1 :(得分:1)
你有2个问题: 1.你欺骗编译器调用一个不退出的函数 2.在确保线程真正启动之前保留构造函数。
对于1:使用模板。通过需要简单void run()
的跑步者课程
对于2:使用bool来确保线程启动。你甚至可以把它交给你的跑步者:void run(bool * started);
结果(没有状态bool注入跑步者的版本):
template < class runner_class>
class ConThread {
public:
ConThread() // respect order of init
:started_(false)
, runner_()
, t_([this]
{
started_ = true;
runner_.run();
})
{
while (!started_); // wait thread is REALLY started ...
}
~ConThread()
{
if (t_.joinable()) {
t_.join();
}
}
std::thread& get() { return t_; };
private: // beware: order of declaration is important here
std::atomic_bool started_;
runner_class runner_;
std::thread t_;
};
答案 2 :(得分:0)
在C ++中,基类在自动设置派生类后无法运行代码;没有&#34; post constru&#34;默认情况下调用。
由于派生类的工作函数在完全构造派生类之前无效,这意味着基类不能安排它运行,除非派生类明确说明它何时准备好并完全构造。< / p>
他们通过std线程来解决这个问题,方法是将所需的行为作为参数注入线程的构造函数。现在bahaviour完全由时间线程构造,因此线程可以自由安排它运行。
这意味着我们不使用继承,至少不是基于C ++虚拟函数表的默认内置继承。但是,不使用语言提供的OO并不意味着您的代码不是OO。
我们可以通过几种方式将这种语言转换为OO提供的语言;要求你们都从你的线程接口继承,并在你的类型下有一个模板派生的助手来解决问题。
但也许更好的是遵循C ++ std模式并将执行对象与执行对象分开是一个好主意。具有可执行对象的概念(可以像&#34;具有operator()&#34;或更复杂的那样简单),以及消耗这些可执行对象的线程抽象。这两个问题都非常复杂,您的代码可能更清晰。
答案 3 :(得分:0)
感谢所有反馈。这就是我最终要做的事情。
//
// Created by pbeerken on 5/18/17.
//
#ifndef LIBCONNECT_CONTHREAD_H
#define LIBCONNECT_CONTHREAD_H
#include <thread>
#include <future>
class ConRunnable2
{
public:
virtual void doWork ()=0;
void requestToTerminate () {requestedToTerminate_=true;};
protected:
bool requestedToTerminate_=false;
};
template <class ClassToRun>
class ConThread2
{
public:
//constructor forwards arguments to the ClassToRun constructor
template <typename ... Arguments > ConThread2 (Arguments ... args)
:toRun_ (args ...)
, t_ ([this] {
started_.set_value();
toRun_.doWork();
})
{
started_.get_future().wait(); //wait till the thread is really started
}
~ConThread2()
{
toRun_.requestToTerminate ();
if (t_.joinable ()){
t_.join ();
}
}
void requestToTerminate () {toRun_.requestToTerminate ();};
std::thread& getThread () {return t_;};
ClassToRun& get () {return toRun_;};
private:
std::promise <void> started_;
ClassToRun toRun_;
std::thread t_;
};
#endif //LIBCONNECT_CONTHREAD_H
通过了这个测试:
#include <iostream>
#include <gtest/gtest.h>
#include "../ConThread.h"
#include <chrono>
#include <future>
class MyClassToBeRun: public ConRunnable2
{
public:
MyClassToBeRun (int loopSize)
:loopSize_ (loopSize)
{};
void doWork () override
{
for (long j=0; j<loopSize_ && requestedToTerminate_==false; j++)
{
++i_;
}
p_.set_value();
}
long i_=0;
long loopSize_=0;
std::promise <void> p_;
};
TEST(MyConThread2Test, TestThatItRunsInASeparateThread)
{
ConThread2<MyClassToBeRun> ct (10000);
ct.get ().p_.get_future ().wait ();
EXPECT_EQ (10000,ct.get ().i_);
}
TEST(MyConThread2Test, TestThatCanBeDestroyed)
{
ConThread2<MyClassToBeRun> ct (1'000'000'000);
}