Otra forma de dispararle a la pierna usando std :: thread

El estándar C ++ 11 introdujo el mecanismo de soporte de subprocesos estándar en el lenguaje (a menudo se denominan secuencias, pero esto crea confusión con el término secuencias, por lo que utilizaré el término original en inglés en la transcripción rusa). Sin embargo, como cualquier mecanismo en C ++, este tiene una serie de trucos, sutilezas y formas completamente nuevas de dispararle a la pierna. Recientemente, apareció una traducción de un artículo sobre 20 de estos métodos en Habré, pero esta lista no es exhaustiva. Quiero hablar sobre otro método relacionado con la inicialización de instancias std::thread en constructores de std::thread .


Aquí hay un ejemplo simple del uso de std::thread :


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

En este ejemplo más simple, el código parece correcto, pero hay un PERO curioso: al momento de llamar al constructor std::thread instancia de la clase Usage aún no se ha construido por completo. Por lo tanto, se puede llamar a Usage::run() para una instancia, algunos de los campos de los cuales (declarados después del campo std::thread ) aún no se han inicializado, lo que, a su vez, puede conducir a UB. Esto puede ser bastante obvio en un pequeño ejemplo donde el código de clase cabe en la pantalla, pero en proyectos reales esta trampa puede ocultarse detrás de una estructura de herencia ramificada. Vamos a complicar un poco el ejemplo de demostración:


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

A primera vista, el código también parece bastante normal, además, casi siempre funcionará como se espera ... hasta que las estrellas se sumen y se BadUsage::run() antes de que ptr_ inicialice ptr_ . Para demostrar esto, agregue un pequeño retraso antes de la inicialización:


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

En este caso, llamar a BadUsage::run() da como resultado un error de segmentación , y valgrind se queja de acceder a la memoria no inicializada.


Para evitar tales situaciones, hay varias soluciones. La opción más fácil es usar la inicialización de dos fases:


 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