Вопрос по c++, destructor, multithreading, boost – Это хорошая идея, чтобы закрыть член потока класса в деструкторе класса?

15

Каков наилучший способ закрыть поток Boost, управляемый классом C ++, когда пришло время уничтожить объект этого класса? У меня есть класс, который создает и запускает поток на строительство и предоставляет общественностиWake() метод, который пробуждает поток, когда пришло время выполнить некоторую работу.Wake() метод использует мьютекс Boost и условную переменную Boost для сигнализации потока; процедура потока ожидает переменную условия, затем выполняет работу и возвращается к ожиданию.

На данный момент я закрыл этот поток в деструкторе класса, используя булеву переменную-член в качестве & quot; идущего & quot; флаг; Я очищаю флаг и затем вызываю notify_one () для условной переменной. Затем процедура потока просыпается и замечает, что "выполняется" ложно и возвращает. Вот код:

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

Является ли хорошей идеей закрыть поток в деструкторе класса, как этот, или я должен предоставить открытый метод, который позволяет пользователю сделать это до уничтожения объекта, когда есть больше возможностей для обработки ошибок и / или принудительно уничтожить поток, если процедура потока не может вернуться чисто или вовремя?

Очистка беспорядка моего объекта в его деструкторе привлекательна, так как он потребует меньше внимания к деталям от пользователя (абстракция, ура!), Но мне кажется, что я должен делать вещи только в деструкторе, если я могу гарантировать, что он будет полностью занят. ответственность за успешную и тщательную очистку, и есть небольшая вероятность того, что когда-нибудь код за пределами класса должен будет знать, был ли поток полностью закрыт.

Кроме того, механизм, который я использую - запись в переменную-член в объекте в стеке одного потока и чтение этой переменной в другом потоке - безопасный и вменяемый?

Tricky. Я полагаю, что это во многом зависит от того, какую семантику владения вы предпочитаете. Одна возможность состоит в том, чтобы сделать деструктор закрытым, чтобы пользовательский код не мог просто безответственно покидать потоки; они должны закрывать их явно и т. д. Кажется, что это немного хлопотно. То, как вы останавливаете поток, кажется вполне приемлемым, хотя я не знаком с библиотекой повышения потоков. Могnotify_one или жеjoin бросить исключения? Rook
Вы должны задать себе вопрос В. Что произойдет, если создаются временные копии вашего класса? Повлияет ли это на поведение вашей программы? Вы следуете Правилу Три / Пять? Alok Save
@Als Спасибо за указание на это; класс предназначен для создания экземпляра только один раз. Функции C ++ используются только для того, чтобы скрыть детали и гарантировать чистоту. Я обновил Q частной копией ctor и operator =, чтобы прояснить это (и добавил их в свой код - еще раз спасибо!) bythescruff
@bythescruff: ради безопасности, вы должныdeclare они обаprivate и никогдаdefine их. Это защитило бы выдачу ошибок, если бы они использовались каким-то противным и странным способом. Alok Save
Поскольку вы «присоединяетесь», это нормально, если не считать возражений против временных заявлений Алса. Обратите внимание, однако, что это заблокирует уничтожение. Деструкторы работают строго последовательно, поэтому потоки также отключаются строго последовательно. Если на нескольких работниках выполняются длительные работы, это может занятьnoticeable время. Однако не поддавайтесь искушению не присоединиться, иначе вы увидите сбои потоков, просыпающихся во время или после того, как вы разрушили структуры, ссылающиеся на них ... Я говорю об этом на собственном опыте. Таким образом, может быть хорошим вариантом иметь «раннее уведомление». механизм. Damon

Ваш Ответ

1   ответ
40

которые класс создает, когда класс уничтожается, даже если один из ресурсов является потоком. Если ресурс создан явно через пользовательский вызов, такой какWorker::Start(), тогда также должен быть явный способ освободить его, такой какWorker::Stop(), Также было бы неплохо выполнить очистку в деструкторе в случае, если пользователь не вызываетWorker::Stop() и / или предоставить пользователю вспомогательный класс с областью действия, который реализуетRAII-идиом, ссылаясьWorker::Start() в своем конструкторе иWorker::Stop() в своем деструкторе. Однако, если распределение ресурсов выполняется неявно, например, вWorker конструктор, то освобождение ресурса также должно быть неявным, оставляя деструктора в качестве основного кандидата на эту ответственность.

Destruction

Давайте рассмотримWorker::~Worker(), Общее правило заключается вне бросать исключения в деструкторы, ЕслиWorker объект находится в стеке, который разматывается из другого исключения, иWorker::~Worker() выдает исключение, затемstd::terminate() будет вызвано, убивая приложение. В то время какWorker::~Worker() не вызывает явное исключение, важно учитывать, что некоторые вызываемые им функции могут выдавать:

m_Condition.notify_one() does not throw. m_pThread->join() could throw boost::thread_interrupted.

Если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 */ 
  }
}
Concurrency

Управление потоками может быть сложным. Важно определить точное желаемое поведение функций, таких какWorker::Wake(), а также понять поведение типов, которые облегчают многопоточность и синхронизацию. Например,boost::condition_variable::notify_one() не имеет никакого эффекта, если ни один поток не заблокирован вboost::condition_variable::wait(), Давайте рассмотрим возможные параллельные пути дляWorker::Wake().

Ниже приведена грубая попытка составить схему параллелизма для двух сценариев:

Order-of-operation occurs from top-to-bottom. (i.e. Operations at the top occur before operations at the bottom. Concurrent operations are written on the same line. < and > are used to highlight when one thread is waking up or unblocking another thread. For example A > B indicates that thread A is unblocking thread B.

Scenario: 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()

Result: Worker::Wake() возвращается довольно быстро, иWorker::ThreadProc пробеги.

Scenario: 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...

Result: Worker::Wake() заблокирован какWorker::ThreadProc работал, но не работал, так как отправил уведомлениеm_Condition когда никто не ждал этого.

Это не особо опасно дляWorker::Wake(), но это может вызвать проблемы вWorker::~Worker(), ЕслиWorker::~Worker() работает покаWorker::ThreadProc делает работу, тоWorker::~Worker() может блокироваться на неопределенный срок при присоединении к потоку, так как поток может не ожидатьm_Condition в точке, в которой оно было уведомлено, иWorker::ThreadProc только чекиm_Running после того, как это сделано в ожиданииm_Condition.

Working Towards a Solution

В этом примере давайте определим следующие требования:

Worker::~Worker() will not cause std::terminate() to be invoked. Worker::Wake() will not block while Worker::ThreadProc is doing work. If Worker::Wake() is called while Worker::ThreadProc is not doing work, then it will notify Worker::ThreadProc to do work. If Worker::Wake() is called while Worker::ThreadProc is doing work, then it will notify Worker::ThreadProc to perform another iteration of work. Multiple calls to Worker::Wake() while Worker::ThreadProc is doing work will result in Worker::ThreadProc performing a single additional iteration of work.

Код:

#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 имеетконструктор по умолчанию это будет относиться кNot-a-Thread, и этодвигаться переуступке.

Вот это да. Upvote для вас, и ваш любимый вопрос, это ответ, который, я уверен, я вернусь к куче раз.

Похожие вопросы