处理 std::线程包装器类的立即销毁

Handle immediate destruction of a std::thread wrapper class

本文关键字:std 线程 包装 处理      更新时间:2023-10-16

我正在为 std::thread 编写一个 OO 包装器。代码的简化版本如下所示。这个类的问题在于,如果它立即被销毁,它可能会引发错误,因为在销毁类的同时从线程调用doWork(调用纯虚拟方法)。

测试用例显示在底部。

如何使这门课更安全?如果MyConThread具有从MyConThread::d oWork使用的成员变量,则更复杂的示例将更糟。

我意识到我在启动时遇到了类似的问题,可以在构造派生类之前调用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 ()
}

首先,无论线程对象是否被销毁,程序都会崩溃。检查起来非常容易,只需在创建对象后插入一些延迟:

using namespace std::chrono_literals;
TEST(MyConThreadTest, TestThatCanBeDestroyed)
{
MyConThread mct ();
std::this_thread::sleep_for(100s);
}

发生崩溃是因为您正在从构造函数调用虚拟方法,这通常是一个非常糟糕的主意。基本上,在C++对象是按从基到派生的顺序创建的,当您在 ctor 中调用纯虚拟方法时,还无法处理重载(因为派生尚未构造)。也看到这个答案。

所以,第一条规则:永远不要从构造函数或析构函数调用虚拟方法(无论是纯方法还是定义方法)。

我认为在这里解决此问题的最简单方法是添加实际启动线程start方法。喜欢这个:

ConThread()
{
}
void start()
{
t_ = std::thread(doWorkInternal, this);
}

一般来说,我不喜欢将逻辑和线程对象混合在一起的想法,因为这样做违反了单一责任原则。你的对象做两件事 - 它是一个线程,它也有你自己的逻辑。通常最好将它们分开处理,这就是为什么std::thread提供了通过构造将逻辑"传递"到其中的方法,并且它不是为用作基类而设计的。我找到了一篇关于这个的好文章,它是关于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、函数指针和参数等)。

你有 2 个问题: 1. 您正在欺骗编译器调用非退出函数 2. 在确保线程真正启动之前保留构造函数。

对于 1:使用模板。通过需要简单void run()的跑步者类 对于 2:使用 bool 来确保线程启动。您甚至可以将其交给您的跑步者:void run(bool * started);

结果(没有状态布尔值注入到运行器的版本):

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_;
};

在C++基类在自动设置派生类后无法运行代码;默认情况下没有"post construct"调用。

由于派生类的工作函数在派生类完全构造之前无效,这意味着基类无法计划它运行,除非派生类明确声明它何时准备就绪并完全构造。

他们使用 std 线程解决这个问题的方式是通过将所需的行为作为参数注入线程的构造函数。 现在巴哈维完全由时间线程解释,所以线程可以自由地安排它运行。

这意味着我们没有使用继承,至少没有使用默认内置的基于虚函数表C++继承。 但是,不使用语言提供的 OO 并不意味着您的代码不是 OO。

我们可以通过几种方式将其硬塞到提供OO的语言中;要求你们都从线程接口继承,并在你的类型下有一个模板派生的助手来启动事情。

但也许更好的是遵循C++ std 模式,将执行的对象与执行对象分开是一个更好的主意。 具有可执行对象的概念(可以像"具有运算符()"一样简单,也可以更复杂),以及使用这些可执行对象的线程抽象。 这两个问题都足够复杂,您的代码可能更干净。

感谢您的所有反馈。这就是我最终所做的。

//
// 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);
}