Outra maneira de fotografar sua perna usando std :: thread

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(); // ... 

Source: https://habr.com/ru/post/pt444464/


All Articles