管理派生类中的线程生命周期

Managing thread life-cycle in derived class

本文关键字:线程 生命 周期 派生 管理      更新时间:2023-10-16

我有一个类,它充当多个同步事件处理策略的接口。现在,我希望这些策略能够异步处理事件。为了尽量减少代码重构,每个策略都有自己的内部线程用于异步事件处理。我主要关心的是如何管理这个线程的生命周期。派生的策略类是在代码库周围构造和销毁的,因此很难在策略类之外管理线程生命周期(启动/停止)。

最后我写了下面的代码:
#include <iostream>
#include <cassert>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
struct Base
{
    virtual ~Base()
    {
        std::cout << "In ~Base()" << std::endl;
        // For testing purpose: spend some time in Base dtor
        boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
    }
    virtual void processEvents() = 0;
    void startThread()
    {
        if(_thread)
        {
            stopThread();
        }
        _thread.reset(new boost::thread(&Base::processEvents, this));
        assert(_thread);
    }
    void stopThread()
    {
        if(_thread)
        {
            std::cout << "Interrupting and joining thread" << std::endl;
            _thread->interrupt();
            _thread->join();
            _thread.reset();
        }
    }
    boost::shared_ptr<boost::thread> _thread;
};
struct Derived : public Base
{
    Derived()
    {
        startThread();
    }
    virtual ~Derived()
    {
        std::cout << "In ~Derived()" << std::endl;
        // For testing purpose: make sure the virtual method is called while in dtor
        boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
        stopThread();
    }
    virtual void processEvents()
    {
        try
        {
            // Process events in Derived specific way
            while(true)
            {
                // Emulated interruption point for testing purpose
                boost::this_thread::sleep(boost::posix_time::milliseconds(100));
                std::cout << "Processing events..." << std::endl;
            }
        }
        catch (boost::thread_interrupted& e)
        {
            std::cout << "Thread interrupted" << std::endl;
        }
    }
};
int main(int argc, char** argv)
{
    Base* b = new Derived;
    delete b;
    return 0;
}
如您所见,线程被中断并在Derived类析构函数中连接。Stackoverflow上的许多评论认为,在析构函数中加入线程是一个坏主意。然而,考虑到线程生命周期必须通过派生类的构造/销毁来管理的约束,我找不到一个更好的主意。有人有更好的建议吗?

在类被销毁时释放类创建的资源是一个好主意,即使其中一个资源是线程。然而,当在析构函数中执行任何重要任务时,通常值得花时间全面检查其含义。


析构函数

一般规则是不要在析构函数中抛出异常。如果Derived对象位于从另一个异常展开的堆栈上,并且Derived::~Derived()抛出异常,则将调用std::terminate(),从而终止应用程序。虽然Derived::~Derived()没有显式抛出异常,但重要的是要考虑到它调用的一些函数可能会抛出异常,例如_thread->join()

如果std::terminate()是期望的行为,则不需要更改。但是,如果不需要std::terminate(),则捕获boost::thread_interrupted并抑制它。

try
{
  _thread->join();
}
catch (const boost::thread_interrupted&)
{
  /* suppressed */ 
}

继承

看起来好像继承被用于代码重用,并通过将异步行为隔离到Base层次结构内部来最小化代码重构。然而,一些样板逻辑也在Dervied中。由于从Base派生的类已经不得不更改,我建议考虑聚合或CRTP,以尽量减少这些类中的样板逻辑和代码的数量。

例如,可以引入helper类型来封装线程逻辑:
class AsyncJob
{
public:
  typedef boost::function<void()> fn_type;
  // Start running a job asynchronously.
  template <typename Fn>
  AsyncJob(const Fn& fn)
    : thread_(&AsyncJob::run, fn_type(fn))
  {}
  // Stop the job.
  ~AsyncJob()
  {
    thread_.interrupt();
    // Join may throw, so catch and suppress.
    try { thread_.join(); }
    catch (const boost::thread_interrupted&) {}
  }
private: 
  //  into the run function so that the loop logic does not
  // need to be duplicated.
  static void run(fn_type fn)
  {
    // Continuously call the provided function until an interrupt occurs.
    try
    {
      while (true)
      {
        fn();
        // Force an interruption point into the loop, as the user provided
        // function may never call a Boost.Thread interruption point.
        boost::this_thread::interruption_point();
      }
    }
    catch (const boost::thread_interrupted&) {}
  }
  boost::thread thread_;
};

这个辅助类可以在Derived的构造函数中聚合和初始化。它消除了对许多样板代码的需要,并且可以在其他地方重用:

struct Derived : public Base
{
    Derived()
      : job_(boost::bind(&Base::processEvents, this))
    {}
    virtual void processEvents()
    {
      // Process events in Derived specific way
    }
private:
  AsyncJob job_;
};

另一个关键点是AsyncJob强制Boost。线程中断点进入循环逻辑。作业关闭逻辑是根据中断点实现的。因此,在迭代期间到达中断点是至关重要的。否则,如果用户代码从未到达中断点,则可能导致死锁。


检查是线程的生命周期必须与对象的生命周期相关联,还是异步事件处理需要与对象的生命周期相关联。如果是后者,那么可能值得考虑使用线程池。线程池可以对线程资源提供更细粒度的控制,例如施加最大限制,以及最小化浪费的线程数量,例如不做任何事情的线程或花费在创建/销毁寿命较短的线程上的时间。

例如,考虑一个用户创建一个包含500个Dervied类的数组的情况。是否需要500个线程来处理500个策略?或者25个线程可以处理500个策略?请记住,在某些系统上,线程的创建/销毁可能非常昂贵,甚至可能存在操作系统施加的最大线程限制。


总之,检查权衡,并确定哪些行为是可接受的。尽量减少代码重构是很困难的,特别是在更改线程模型时,这对代码库的各个领域都有影响。完美的解决方案很少可以获得,所以要确定涵盖大多数情况的解决方案。一旦明确定义了支持的行为,就可以修改现有的代码,使其符合支持的行为。