Cara lain untuk menembak kaki Anda menggunakan std :: thread

Standar C ++ 11 memperkenalkan mekanisme dukungan thread standar ke dalam bahasa (mereka sering disebut stream, tetapi ini menciptakan kebingungan dengan istilah stream, jadi saya akan menggunakan istilah bahasa Inggris asli dalam transkripsi Rusia). Namun, seperti mekanisme apa pun di C ++, mekanisme ini membawa sejumlah trik, kehalusan, dan cara yang sama sekali baru untuk menembak kaki Anda. Baru-baru ini, terjemahan sebuah artikel tentang 20 metode seperti itu muncul di HabrΓ©, tetapi daftar ini tidak lengkap. Saya ingin berbicara tentang metode lain yang terkait dengan menginisialisasi contoh std::thread di konstruktor std::thread .


Berikut adalah contoh sederhana menggunakan std::thread :


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

Dalam contoh paling sederhana ini, kode terlihat benar, tetapi ada satu yang TETAPI: pada saat memanggil konstruktor std::thread turunan dari kelas Usage belum sepenuhnya dibangun. Dengan demikian, Usage::run() dapat dipanggil sebagai contoh, beberapa bidang yang (dinyatakan setelah bidang std::thread ) belum diinisialisasi, yang, pada gilirannya, dapat mengarah ke UB. Ini bisa sangat jelas dalam contoh kecil di mana kode kelas cocok di layar, tetapi dalam proyek nyata perangkap ini dapat disembunyikan di balik struktur warisan bercabang. Mari kita sedikit mempersulit contoh untuk demonstrasi:


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

Pada pandangan pertama, kode ini juga terlihat sangat normal, apalagi, hampir selalu berfungsi seperti yang diharapkan ... sampai bintang-bintang bertambah sehingga BadUsage::run() dipanggil sebelum ptr_ diinisialisasi. Untuk menunjukkan ini, tambahkan sedikit keterlambatan sebelum inisialisasi:


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

Dalam kasus ini, panggilan BadUsage::run() menghasilkan kesalahan Segmentasi , dan valgrind mengeluh tentang mengakses memori yang tidak diinisialisasi.


Untuk menghindari situasi seperti itu, ada beberapa solusi. Opsi termudah adalah menggunakan inisialisasi dua fase:


 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/id444464/


All Articles