La norme C ++ 11 a introduit le mécanisme de prise en charge des threads standard dans la langue (ils sont souvent appelés flux, mais cela crée une confusion avec le terme flux, donc j'utiliserai le terme anglais d'origine dans la transcription russe). Cependant, comme tout mécanisme en C ++, celui-ci comporte un certain nombre d'astuces, de subtilités et de façons complètement nouvelles de tirer sur votre jambe. Récemment, une traduction d'un article sur 20 de ces méthodes est parue sur Habré, mais cette liste n'est pas exhaustive. Je veux parler d'une autre méthode de ce type liée à l'initialisation des instances std::thread
dans std::thread
constructeurs de std::thread
.
Voici un exemple simple d'utilisation de std::thread
:
class Usage { public: Usage() : th_([this](){ run(); }) {} void run() { // Run in thread } private: std::thread th_; };
Dans cet exemple le plus simple, le code semble correct, mais il y a un curieux MAIS: au moment d'appeler le constructeur std::thread
instance de la classe Usage n'a pas encore été complètement construite. Ainsi, Usage::run()
peut être appelé pour une instance, dont certains des champs (déclarés après le champ std::thread
) n'ont pas encore été initialisés, ce qui, à son tour, peut conduire à UB. Cela peut être assez évident dans un petit exemple où le code de classe tient à l'écran, mais dans les projets réels, ce piège peut être caché derrière une structure d'héritage ramifiée. Compliquons un peu l'exemple de démonstration:
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_; };
À première vue, le code semble également tout à fait normal, de plus, il fonctionnera presque toujours comme prévu ... jusqu'à ce que les étoiles s'additionnent pour que BadUsage::run()
appelé avant l'initialisation de ptr_
. Pour illustrer cela, ajoutez un petit délai avant l'initialisation:
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_; };
Dans ce cas, l'appel de BadUsage::run()
entraîne une erreur de segmentation et valgrind se plaint d'accéder à la mémoire non initialisée.
Pour éviter de telles situations, il existe plusieurs solutions. L'option la plus simple consiste à utiliser l'initialisation en deux phases:
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(); // ...