Как изящно остановить/уничтожить поток с блокирующим вызовом при вызове деструктора С++?

В следующем классе рабочий поток запускается внутри конструктора. У рабочего есть блокирующий вызов очереди.

Он работает, как и ожидалось, но когда объект AsyncQueue выходит за пределы области видимости (по какой-либо причине), вызывается его деструктор. Также вызывается деструктор объекта simple_queue (что я проверил отладкой).

Но что происходит с рабочим? Потому что он все еще ожидает блокирующего вызова очереди!

Я заметил, что без вызова impl_thread_.detach(); в деструкторе выполнение падает. Однако я не знаю, является ли это решением вообще. Что я дополнительно не понимаю: хотя объект очереди уничтожен, вызов блокировки не вызывает исключение - фактически я установил точку останова в обработчике catch. Так что же здесь происходит и как правильно реализовать этот сценарий? Я глубоко чувствую, что то, что я здесь делаю, не совсем так, как должно быть ;-)

    template<typename T>
    class AsyncQueue
    {
    public:
        AsyncQueue() : impl_thread_(&AsyncQueue::worker, this)
        {
            tq_ = std::shared_ptr<simple_queue<T>>(new simple_queue<T>);
            impl_thread_.detach();
        }
        //~AsyncQueue() = default;

        ~AsyncQueue() {
            std::cout << "[" << boost::this_thread::get_id() << "] destructor AsyncQueue" << std::endl;
            return;
        }

    private:
        std::thread impl_thread_;
        std::shared_ptr<simple_queue<T>> tq_;

        void worker()
        {
            try {
                while (true)
                {
                    boost::optional<T> item = tq_->deq(); // blocks

                    ...
                    ...
                    ...
                }

            }
            catch (exception const& e) {
                return;
            }
        }

    public:
    ...
    ...


    };

person MichaelW    schedule 07.02.2020    source источник
comment
Вопрос ОТ: Я не думаю, что это безопасный способ начать тему: : impl_thread_(&AsyncQueue::worker, this). Проблема в том, что если позже в конструкторе возникнет исключение (например, из new), может быть вызван деструктор потока joinable impl_thread_, что приведет к std::terminate.   -  person Daniel Langr    schedule 07.02.2020
comment
Не добавляйте код, который не имеет значения (эти два возврата в деструкторе и блоке catch). Вы все равно вернетесь из функции, как только будет запущено последнее выражение.   -  person Aconcagua    schedule 07.02.2020
comment
Ваш поток запускается и обращается к tq_ до того, как этот элемент также будет гарантированно установлен. Кроме того, есть ли особая причина, по которой tq_ является общим указателем? Просто используйте простой объект. При объявлении до impl_thread_ вы также разрешаете условие гонки.   -  person Ulrich Eckhardt    schedule 07.02.2020
comment
@Ulrich: Да, ты прав. Однако изменение конструктора не решает моей проблемы, которая больше связана с разрушением. Использование простого объекта вместо общего указателя также.   -  person MichaelW    schedule 07.02.2020


Ответы (1)


Самый простой способ, если вы можете, - это в вашем деструкторе отправить токен остановки в очередь eq и проверить токен остановки в рабочем потоке, чтобы выйти из него. Сначала удалите отсоединение.

~AsyncQueue() {
  _eq.add(stopToken); // what ever your can use here. else use an atomic bool 
  std::cout << "[" << boost::this_thread::get_id() << "] destructor AsyncQueue" << std::endl;
  impl_thread_.join();
}

(непроверенный, неполный)

person Surt    schedule 07.02.2020