The C ++ 11 standard brought a standard thread support mechanism to 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 with it a number of tricks, subtleties and completely new ways to shoot yourself a leg. Recently, a translation of an article about 20 such methods appeared on Habré, but this list is not exhaustive. I want to tell about one more such method connected with initialization of instances of std::thread
in class constructors.
Here is a simple example of using 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 the std::thread
constructor is called, an instance of the Usage class is not yet fully constructed. Thus, Usage::run()
can be called for an instance, some of whose fields (declared after the std::thread
field) 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 in the screen, but in real projects this trap can be hidden behind the spreading structure of inheritance. Let's slightly complicate an example to demonstrate:
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 develop so that BadUsage::run()
called before the ptr_
initialized. 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 BadUsage::run()
call leads to the Segmentation fault , and valgrind complains about uninitialized memory access.
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<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(); // ...
Source: https://habr.com/ru/post/444464/
All Articles