O padrão C ++ 11 introduziu o mecanismo de suporte de encadeamento padrão no idioma (eles geralmente são chamados de fluxos, mas isso cria confusão com o termo fluxos, portanto, usarei o termo original em inglês na transcrição em russo). No entanto, como qualquer mecanismo em C ++, este contém vários truques, sutilezas e maneiras completamente novas de fotografar sua perna. Recentemente, uma tradução de um artigo sobre 20 desses métodos apareceu em Habré, mas essa lista não é exaustiva. Eu quero falar sobre outro método relacionado à inicialização de instâncias std::thread
em construtores de std::thread
.
Aqui está um exemplo simples de usar std::thread
:
class Usage { public: Usage() : th_([this](){ run(); }) {} void run() { // Run in thread } private: std::thread th_; };
Neste exemplo mais simples, o código parece correto, mas há um curioso, MAS: no momento de chamar o construtor std::thread
instância da classe Usage ainda não foi completamente construída. Portanto, Usage::run()
pode ser chamado para uma instância, alguns dos campos (declarados após o campo std::thread
) ainda não foram inicializados, o que, por sua vez, pode levar ao UB. Isso pode ser bastante óbvio em um pequeno exemplo em que o código da classe se encaixa na tela, mas em projetos reais essa armadilha pode ser oculta por trás de uma estrutura de herança ramificada. Vamos complicar um pouco o exemplo de demonstração:
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_; };
À primeira vista, o código também parece bastante normal; além disso, quase sempre funcionará como esperado ... até que as estrelas se BadUsage::run()
modo que BadUsage::run()
chamado antes da inicialização do ptr_
. Para demonstrar isso, adicione um pequeno atraso antes da inicialização:
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_; };
Nesse caso, chamar BadUsage::run()
resulta em uma falha de segmentação e o valgrind reclama do acesso à memória não inicializada.
Para evitar tais situações, existem várias soluções. A opção mais fácil é usar a inicialização em duas 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(); // ...