Eine andere Möglichkeit, Ihr Bein mit std :: thread zu schießen

Der C ++ 11-Standard führte den Standard-Thread-Unterstützungsmechanismus in die Sprache ein (sie werden oft als Streams bezeichnet, dies führt jedoch zu Verwechslungen mit dem Begriff Streams, sodass ich den ursprünglichen englischen Begriff in der russischen Transkription verwenden werde). Wie jeder Mechanismus in C ++ enthält dieser jedoch eine Reihe von Tricks, Feinheiten und völlig neuen Möglichkeiten, um Ihr Bein zu schießen. Kürzlich erschien auf Habré eine Übersetzung eines Artikels über 20 solcher Methoden , aber diese Liste ist nicht vollständig. Ich möchte über eine andere solche Methode sprechen, die sich auf die Initialisierung von std::thread Instanzen in std::thread bezieht.


Hier ist ein einfaches Beispiel für die Verwendung von std::thread :


 class Usage { public: Usage() : th_([this](){ run(); }) {} void run() { // Run in thread } private: std::thread th_; }; 

In diesem einfachsten Beispiel sieht der Code korrekt aus, aber es gibt einen merkwürdigen ABER: Zum Zeitpunkt des Aufrufs des Konstruktors std::thread Instanz der Usage-Klasse noch nicht vollständig erstellt. Daher kann Usage::run() für eine Instanz aufgerufen werden, deren Felder (deklariert nach dem Feld std::thread ) noch nicht initialisiert wurden, was wiederum zu UB führen kann. Dies kann in einem kleinen Beispiel, in dem der Klassencode auf den Bildschirm passt, ziemlich offensichtlich sein, aber in realen Projekten kann diese Falle hinter einer verzweigten Vererbungsstruktur versteckt sein. Lassen Sie uns das Beispiel für die Demonstration etwas komplizieren:


 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_; }; 

Auf den ersten Blick sieht der Code auch ganz normal aus, außerdem funktioniert er fast immer wie erwartet ... bis sich die Sterne addieren, so dass BadUsage::run() aufgerufen wird, bevor ptr_ initialisiert wird. Um dies zu demonstrieren, fügen Sie vor der Initialisierung eine kleine Verzögerung hinzu:


 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 diesem Fall führt der Aufruf von BadUsage::run() zu einem Segmentierungsfehler , und valgrind beschwert sich über den Zugriff auf nicht initialisierten Speicher.


Um solche Situationen zu vermeiden, gibt es verschiedene Lösungen. Die einfachste Option ist die Verwendung der zweiphasigen Initialisierung:


 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/de444464/


All Articles