Стандарт C++11 принёс в язык стандартный механизм поддержки тредов (их часто называют потоками, но это создаёт путаницу с термином streams, так что я буду использовать оригинальный англоязычный термин в русской транскрипции). Однако, как и любой механизм в C++, этот несёт в себе ряд хитростей, тонкостей и совершенно новых способов отстрелить себе ногу. Недавно на Хабре появлялся перевод статьи про 20 таких способов, но список этот исчёрпывающим не является. Я хочу рассказать ещё об одном таком способе, связанном с инициализацией экземпляров std::thread в конструкторах классов.
Вот простой пример использования std::thread:
class Usage { public: Usage() : th_([this](){ run(); }) {} void run() { // Run in thread } private: std::thread th_; };
В этом простейшем примере код выглядит корректным, но есть одно любопытное НО: в момент вызова конструктора std::thread экземпляр класса Usage ещё не сконструирован полностью. Таким образом, Usage::run() может быть вызван для экземпляра, часть полей которого (объявленных после поля std::thread) ещё не инициализированы, что, в свою очередь, может привести к UB. Это может быть достаточно очевидно на небольшом примере, где код класса умещается в экран, но в реальных проектах этот капкан может быть припрятан за развесистой структурой наследования. Немного усложним пример для демонстрации:
class Usage { public: Usage() : th_([this](){ run(); }) {} virtual ~Usage() noexcept {} virtual void run() {} private: std::thread th_; }; class BadUsage : public Usage { public: BadUsage() : ptr_(new char[100]) {} ~BadUsage() { delete[] ptr_; } void run() { std::memcpy(ptr_, "Hello"); } private: char* ptr_; };
На первый взгляд, код тоже выглядит вполне нормально, более того, он и работать почти всегда будет как ожидается… до тех пор, пока звёзды не сложатся так, что BadUsage::run() вызовется раньше, чем инициализируется ptr_. Чтобы это продемонстрировать, добавим крошечную задержку перед инициализацией:
class BadUsage : public Usage { public: BadUsage() : ptr_((std::this_thread::sleep_for(std::chrono::milliseconds(1)), new char[100])) {} ~BadUsage() { delete[] ptr_; } void run() { std::memcpy(ptr_, "Hello", 6); } private: char* ptr_; };
В этом случае вызов BadUsage::run() приводит к Segmentation fault, а valgrind жалуется на обращение к неинициализированной памяти.
Чтобы избежать таких ситуаций, есть несколько вариантов решения. Самый простой вариант — использовать двухфазную инициализацию:
class TwoPhaseUsage { public: TwoPhaseUsage() = default; ~TwoPhaseUsage() noexcept {} void start() { th_.reset(new std::thread([this](){ run(); })); } virtual void run() {} void join() { if (th_ && th_->joinable()) { th_->join(); } } private: std::unique_ptr<std::thread> th_; }; class GoodUsage : public TwoPhaseUsage { public: GoodUsage() : ptr_((std::this_thread::sleep_for(std::chrono::milliseconds(1)), new char[100])) {} ~GoodUsage() noexcept { delete[] ptr_; } void run() { std::memcpy(ptr_, "Hello", sizeof("Hello")); } private: char* ptr_; }; // ... GoodUsage gu; gu.start(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); gu.join(); // ...
