Стандарт 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();
// ...