Another way to shoot your leg using std :: thread

    The C ++ 11 standard introduced the standard thread support mechanism into the language (they are often called streams, but this creates confusion with the term streams, so I will use the original English term in Russian transcription). However, like any mechanism in C ++, this one carries a number of tricks, subtleties, and completely new ways to shoot your leg. Recently, a translation of an article about 20 such methods appeared on Habré , but this list is not exhaustive. I want to talk about another such method related to initializing instances std::threadin class constructors.


    Here is a simple use case std::thread:


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

    In this simplest example, the code looks correct, but there is one curious BUT: at the time of calling the constructor, the std::threadinstance of the Usage class has not yet been completely constructed. Thus, it Usage::run()can be called for an instance, some of the fields of which (declared after the field std::thread) have not yet been initialized, which, in turn, can lead to UB. This can be quite obvious in a small example where the class code fits on the screen, but in real projects this trap can be hidden behind a branchy inheritance structure. Let's complicate the example for demonstration a bit:


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

    At first glance, the code also looks quite normal, moreover, it will almost always work as expected ... until the stars add up so that it is BadUsage::run()called before it is initialized ptr_. To demonstrate this, add a tiny delay before initialization:


    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 this case, the call BadUsage::run()leads to a Segmentation fault , and valgrind complains about accessing uninitialized memory.


    To avoid such situations, there are several solutions. The easiest option is to use two-phase initialization:


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

    Also popular now: