Another way to shoot your leg using std :: thread
The C ++ 11 standard introduced the standard thread support mechanism into the language (they are often called streams, but this creates confusion with the term streams, so I will use the original English term in Russian transcription). However, like any mechanism in C ++, this one carries a number of tricks, subtleties, and completely new ways to shoot your leg. Recently, a translation of an article about 20 such methods appeared on Habré , but this list is not exhaustive. I want to talk about another such method related to initializing instances std::thread
in class constructors.
Here is a simple use case std::thread
:
class Usage {
public:
Usage() : th_([this](){ run(); }) {}
void run() {
// Run in thread
}
private:
std::thread th_;
};
In this simplest example, the code looks correct, but there is one curious BUT: at the time of calling the constructor, the std::thread
instance of the Usage class has not yet been completely constructed. Thus, it Usage::run()
can be called for an instance, some of the fields of which (declared after the field std::thread
) have not yet been initialized, which, in turn, can lead to UB. This can be quite obvious in a small example where the class code fits on the screen, but in real projects this trap can be hidden behind a branchy inheritance structure. Let's complicate the example for demonstration a bit:
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_;
};
At first glance, the code also looks quite normal, moreover, it will almost always work as expected ... until the stars add up so that it is BadUsage::run()
called before it is initialized ptr_
. To demonstrate this, add a tiny delay before initialization:
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_;
};
In this case, the call BadUsage::run()
leads to a Segmentation fault , and valgrind complains about accessing uninitialized memory.
To avoid such situations, there are several solutions. The easiest option is to use two-phase initialization:
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 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();
// ...