В начале недели оказался в ситуации, когда нужно было быстро набросать черновик класса, которому нужна вспомогательная фоновая нить для выполнения некоторых периодических действий. Набросал, вроде показалось, что норм.
Но на следующий день, на свежую голову, пришел к выводу, что с получившимся черновиком не все так хорошо, как хотелось бы. Попробую пояснить что к чему.
Итак, было предложено что-то вроде вот этого:
class background_task_owner { ... // Какое-то актуальное наполнение класса. bool shutdown_{false}; std::mutex shutdown_lock_; std::condition_variable shutdown_cv_; std::thread background_thread_; public: background_task_owner(/* Какие-то прикладные параметры*/) : ... // Инициализация нужных классу полей. // Сразу же запускаем фоновую задачу. , background_thread_{ [this]{ background_thread_body(); } } {} ~background_task_owner() { { std::lock_guard<std::mutex> lock{shutdown_lock_}; shutdown_ = true; shutdown_cv_.notify_one(); } background_thread_.join(); } private: void background_thread_body() { std::unique_lock<std::mutex> lock{shutdown_lock_}; while(!shutdown_) { lock.unlock(); ... // Какие-то действия. lock.lock(); if(!shutdown_) { // Нужно заснуть на какое-то время. // Но проснуться нужно если shutdown_ выставят раньше. shutdown_cv_.wait_for(lock, std::chrono::minutes{20}, [this]{ return shutdown_; }); } } } }; |
Суть простая: в списке инициализации background_task_owner создается фоновая рабочая нить, которая затем останавливется в деструкторе background_task_owner.
Изначально я здесь проблем не видел, но затем подумалось, что такое простое решение не обеспечивает должной безопасности исключений.
Допустим, что конструктор background_task_owner со временем стал сложнее и в нем появились какие-то дополнительные действия после запуска фоновой рабочей нити. И какое-то из этих действий может привести к выбросу исключения.
В таком случае у нас проблема: фоновой нити никто не даст сигнала на завершение работы. Соответственно, никто не будет делать для нее join. Соответственно, когда дойдет дело до разрушения объекта std::thread, связанного с фоновой нитью, программа тупо заломается.
Как мне кажется, нормального решения у этой проблемы нет. Но есть два ключевых момента, которые могут облегчить нам жизнь.
Во-первых, я бы создал вспомогательный объект, который бы отвечал за останов фоновой рабочей нити. Чтобы не приходилось делать это вручную в деструкторе background_task_owner.
Во-вторых, я не стал запускать фоновую нить прямо в списке инициализации экземпляра background_task_owner. Нуждо делать это либо уже в теле конструктора background_task_owner, либо вообще отдельным методом, который пользователь должен вызвать уже после того, как background_task_owner сконструирован. Да, здесь получается что-то вроде two-phase init, что не есть хорошо в общем случае, но конкретно здесь two-phase init может быть уместен.
В общем, с учетом вышесказанного я бы переписал свой черновик хотя бы вот так:
namespace details { struct shutdown_control { bool shutdown_{false}; std::mutex shutdown_lock_; std::condition_variable shutdown_cv_; }; class thread_shutdowner { shutdown_control & control_; std::thread & target_thread_; public: thread_shutdowner( shutdown_control & control, std::thread & target_thread) : control_{control} , target_thread_{target_thread} {} ~thread_shutdowner() { if(target_thread_.joinable()) { { std::lock_guard<std::mutex> lock{control_.shutdown_lock_}; control_.shutdown_ = true; control_.shutdown_cv_.notify_one(); } target_thread_.join(); } } // Этот тип нельзя ни копировать, ни перемещать. thread_shutdowner(const thread_shutdowner &) = delete; thread_shutdowner(thread_shutdowner &&) = delete; thread_shutdowner & operator=(const thread_shutdowner &) = delete; thread_shutdowner & operator=(thread_shutdowner &&) = delete; }; } /* namespace details */ class background_task_owner { ... // Какое-то актуальное наполнение класса. // Порядок следования этих членов класса принципиально важен, // т.к. деструкторы будут вызываться строго в обратном порядке. details::shutdown_control shutdown_control_; std::thread background_thread_; details::thread_shutdowner background_thread_shutdowner_; public: background_task_owner(/* Какие-то прикладные параметры*/) : ... // Инициализация нужных классу полей. // Фоновую задачу сразу не запускаем, но зато сразу // инициализируем background_thread_shutdowner_. , background_thread_shutdowner_{shutdown_control_, background_thread_} // Далее может быть инициализация остальных членов класса. { // Вот теперь мы можем запустить фоновую нить. background_thread_ = std::thread{ [this]{ background_thread_body(); } }; } // Собственный деструктор нам теперь не нужен. private: void background_thread_body() { std::unique_lock<std::mutex> lock{shutdown_control_.shutdown_lock_}; while(!shutdown_control_.shutdown_) { lock.unlock(); ... // Какие-то действия. lock.lock(); if(!shutdown_control_.shutdown_) { // Нужно заснуть на какое-то время. // Но проснуться нужно если shutdown_ выставят раньше. shutdown_control_.shutdown_cv_.wait_for( lock, std::chrono::minutes{20}, [this]{ return shutdown_control_.shutdown_; }); } } } }; |
Морали не будет. Разве что "быстро только кошки родяться", а придумать сходу хорошее и надежное решение даже для простенькой с виду задачки может быть не так просто, как кажется.
4 комментария:
Когда уже в плюсах будут фреймворки, чтобы не писать низкоуровневой многопоточный код :)
И не говори! Доколе?
кому реально надо часто многопоточку делать - уже постоянно используют TBB / SObjectizer (sic!) / свой велосипед. И соотвественно держат всё сложность в уме.
тем кому это надо редко - получают удовольствие от решения задачи. Ну согласитесь же - когда красиво и правильно написали код - приятно же! даже больше чем от применения фреймворка. который ещё подключить надо (подключение сторонних библиотек - вот адище в плюсах. возможно самый большой недостаток языка для меня).
и да - таймаут я бы не 20 минут, а 100мс поставил. сталкивался с весьма странными проблемами когда под виндой _иногда_ не просыпался wait. а после таймаута при заходе в wait срабатывал. с чем связано - так и не понял, но теперь стараюсь бесконечность или длинные таймауты не использовать. VS2019.
Отправить комментарий