在类的析构函数中关闭类的线程成员是一个好主意吗?

时间:2012-06-27 09:26:04

标签: c++ multithreading boost destructor

当该类的对象被销毁时,关闭由C ++类管理的Boost线程的最佳方法是什么?我有一个类,它在构造时创建并启动一个线程,并提供一个公共Wake()方法,当需要做一些工作时唤醒线程。 Wake()方法使用Boost互斥锁和Boost条件变量来表示线程;线程程序等待条件变量,然后完成工作并返回等待。

目前,我在类的析构函数中关闭了这个线程,使用布尔成员变量作为“running”标志;我清除该标志,然后在条件变量上调用notify_one()。然后线程程序唤醒,注意“running”为false,并返回。这是代码:

class Worker
{
public:
    Worker();
    ~Worker();
    void Wake();
private:
    Worker(Worker const& rhs);             // prevent copying
    Worker& operator=(Worker const& rhs);  // prevent assignment
    void ThreadProc();
    bool m_Running;
    boost::mutex               m_Mutex;
    boost::condition_variable  m_Condition;
    boost::scoped_ptr<boost::thread> m_pThread;
};

Worker::Worker()
    : m_Running(true)
    , m_Mutex()
    , m_Condition()
    , m_pThread()
{
    m_pThread.reset(new boost::thread(boost::bind(&Worker::ThreadProc, this)));
}

Worker::~Worker()
{
    m_Running = false;
    m_Condition.notify_one();
    m_pThread->join();
}

void Worker::Wake()
{
    boost::lock_guard<boost::mutex> lock(m_Mutex);
    m_Condition.notify_one();
}

void Worker::ThreadProc()
{
    for (;;)
    {
        boost::unique_lock<boost::mutex> lock(m_Mutex);
        m_Condition.wait(lock);
        if (! m_Running) break;
        // do some work here
    }
}

像这样关闭类的析构函数中的线程是一个好主意,还是应该提供一个公共方法,让用户在对象被销毁之前执行此操作,此时更有可能进行错误处理和/或强制执行如果线程程序未能干净或及时返回,则销毁该线程?

清理我的对象在其析构函数中的混乱是有吸引力的,因为它将需要较少关注用户的细节(抽象,欢呼!)但在我看来,如果我可以保证采取我只应该在析构函数中做一些事情成功彻底地清理事务的全部责任,并且有一天,课外的代码很可能需要知道线程是否被彻底关闭。

此外,我正在使用的机制 - 写入一个线程堆栈中的对象中的成员变量并在另一个线程中读取该变量 - 安全且理智吗?

1 个答案:

答案 0 :(得分:42)

当类被销毁时,释放类创建的资源是个好主意,即使其中一个资源是一个线程。如果资源是通过用户调用显式创建的,例如Worker::Start(),那么还应该有一种明确的方式来释放它,例如Worker::Stop()。如果用户没有调用Worker::Stop()和/或为用户提供实现RAII惯用语的作用助手类,则在析构函数中执行清理也是一个好主意,调用Worker::Start()在其构造函数中,Worker::Stop()在其析构函数中。但是,如果资源分配是隐式完成的,例如在Worker构造函数中,那么资源的释放也应该是隐式的,将析构函数作为此职责的主要候选者。


销毁

让我们检查Worker::~Worker()。一般规则是not throw exceptions in destructors。如果Worker对象位于正在从另一个异常展开的堆栈上,并且Worker::~Worker()抛出异常,则将调用std::terminate(),从而终止该应用程序。虽然Worker::~Worker()没有显式抛出异常,但重要的是要考虑它调用的某些函数可能抛出:

如果std::terminate()是所需行为,则无需进行任何更改。但是,如果不需要std::terminate(),请抓住boost::thread_interrupted并禁止它。

Worker::~Worker()
{
  m_Running = false;
  m_Condition.notify_one();
  try
  {
    m_pThread->join();
  }
  catch ( const boost::thread_interrupted& )
  {
    /* suppressed */ 
  }
}

并发

管理线程可能很棘手。定义像Worker::Wake()这样的函数的确切期望行为以及理解促进线程和同步的类型的行为是很重要的。例如,如果boost::condition_variable::wait()中没有阻止任何线程,则boost::condition_variable::notify_one()无效。让我们检查Worker::Wake()可能的并发路径。

以下是对两种情况的并发性图表的粗略尝试:

  • 操作顺序从上到下发生。 (即顶部的操作发生在底部操作之前。
  • 并发操作写在同一行。
  • <>用于突出显示一个线程唤醒或解除阻塞另一个线程的时间。例如,A > B表示线程A正在解除阻塞线程B

场景Worker::Wake()Worker::ThreadProc()上阻止m_Condition时调用。

Other Thread                       | Worker::ThreadProc
-----------------------------------+------------------------------------------
                                   | lock( m_Mutex )
                                   | `-- m_Mutex.lock()
                                   | m_Condition::wait( lock )
                                   | |-- m_Mutex.unlock()
                                   | |-- waits on notification
Worker::Wake()                     | |
|-- lock( m_Mutex )                | |
|   `-- m_Mutex.lock()             | |
|-- m_Condition::notify_one()      > |-- wakes up from notification
`-- ~lock()                        | `-- m_Mutex.lock() // blocks
    `-- m_Mutex.unlock()           >     `-- // acquires lock
                                   | // do some work here
                                   | ~lock() // end of for loop's scope
                                   | `-- m_Mutex.unlock()

结果Worker::Wake()返回相当快,Worker::ThreadProc就会运行。


情景:在Worker::Wake()未阻止Worker::ThreadProc()时调用了m_Condition

Other Thread                       | Worker::ThreadProc
-----------------------------------+------------------------------------------
                                   | lock( m_Mutex )
                                   | `-- m_Mutex.lock()
                                   | m_Condition::wait( lock )
                                   | |-- m_Mutex.unlock()
Worker::Wake()                     > |-- wakes up
                                   | `-- m_Mutex.lock()
Worker::Wake()                     | // do some work here
|-- lock( m_Mutex )                | // still doing work...
|   |-- m_Mutex.lock() // block    | // hope we do not block on a system call
|   |                              | // and more work...
|   |                              | ~lock() // end of for loop's scope
|   |-- // still blocked           < `-- m_Mutex.unlock()
|   `-- // acquires lock           | lock( m_Mutex ) // next 'for' iteration.
|-- m_Condition::notify_one()      | `-- m_Mutex.lock() // blocked
`-- ~lock()                        |     |-- // still blocked
    `-- m_Mutex.unlock()           >     `-- // acquires lock
                                   | m_Condition::wait( lock )    
                                   | |-- m_Mutex.unlock()
                                   | `-- waits on notification
                                   |     `-- still waiting...

结果Worker::Wake()被阻止Worker::ThreadProc确实有效,但是没有操作,因为当没有人在等待时它向m_Condition发送了通知在它上面。

这对Worker::Wake()并不是特别危险,但它可能会导致Worker::~Worker()出现问题。如果Worker::~Worker()Worker::ThreadProc正在运行时运行,那么Worker::~Worker()可能会在加入线程时无限期地阻塞,因为线程可能无法在m_Condition处等待它系统会收到通知,Worker::ThreadProc仅在m_Running等待完成后m_Condition检查。


寻求解决方案

在此示例中,我们定义以下要求:

  • Worker::~Worker()不会导致std::terminate()被调用。
  • Worker::Wake()正在开展工作时,
  • Worker::ThreadProc不会阻止。
  • 如果在Worker::Wake()无法正常工作时调用Worker::ThreadProc,则会通知Worker::ThreadProc开展工作。
  • 如果在Worker::Wake()正在工作时调用Worker::ThreadProc,则会通知Worker::ThreadProc执行另一次迭代工作。
  • Worker::Wake()工作时多次调用Worker::ThreadProc会导致Worker::ThreadProc执行一次额外的工作。

代码:

#include <boost/thread.hpp>

class Worker
{
public:
  Worker();
  ~Worker();
  void Wake();
private:
  Worker(Worker const& rhs);             // prevent copying
  Worker& operator=(Worker const& rhs);  // prevent assignment
  void ThreadProc();

  enum state { HAS_WORK, NO_WORK, SHUTDOWN };

  state                            m_State;
  boost::mutex                     m_Mutex;
  boost::condition_variable        m_Condition;
  boost::thread                    m_Thread;
};

Worker::Worker()
  : m_State(NO_WORK)
  , m_Mutex()
  , m_Condition()
  , m_Thread()
{
  m_Thread = boost::thread(&Worker::ThreadProc, this);
}

Worker::~Worker()
{
  // Create scope so that the mutex is only locked when changing state and
  // notifying the condition.  It would result in a deadlock if the lock was
  // still held by this function when trying to join the thread.
  {
    boost::lock_guard<boost::mutex> lock(m_Mutex);
    m_State = SHUTDOWN;
    m_Condition.notify_one();
  }
  try { m_Thread.join(); }
  catch ( const boost::thread_interrupted& ) { /* suppress */ };
}

void Worker::Wake()
{
  boost::lock_guard<boost::mutex> lock(m_Mutex);
  m_State = HAS_WORK;
  m_Condition.notify_one();
}

void Worker::ThreadProc()
{
  for (;;)
  {
    // Create scope to only lock the mutex when checking for the state.  Do
    // not continue to hold the mutex wile doing busy work.
    {
      boost::unique_lock<boost::mutex> lock(m_Mutex);
      // While there is no work (implies not shutting down), then wait on
      // the condition.
      while (NO_WORK == m_State)
      {
        m_Condition.wait(lock);
        // Will wake up from either Wake() or ~Worker() signaling the condition
        // variable.  At that point, m_State will either be HAS_WORK or
        // SHUTDOWN.
      }
      // On shutdown, break out of the for loop.
      if (SHUTDOWN == m_State) break;
      // Set state to indicate no work is queued.
      m_State = NO_WORK;
    }

    // do some work here
  }
}

注意:作为个人偏好,我选择不在堆上分配boost::thread,因此,我不需要通过boost::scoped_ptr进行管理。 boost::thread有一个default constructor,它将引用非线程,它是move-assignable