处理std :: thread包装类的立即销毁

时间:2017-05-20 19:55:07

标签: c++ c++11 c++14 stdthread

我正在为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 ()
}

4 个答案:

答案 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);
}