أدخل معيار C ++ 11 آلية دعم سلاسل العمليات القياسية في اللغة (يطلق عليها غالبًا تدفقات ، ولكن هذا يخلق تشويشًا مع مصطلح تدفقات المصطلح ، لذلك سأستخدم المصطلح الإنجليزي الأصلي في النسخ الروسي). ومع ذلك ، مثل أي آلية في C ++ ، فإن هذا الجهاز يحمل عددًا من الحيل والدقة ، وطرق جديدة تمامًا لإطلاق النار على ساقك. في الآونة الأخيرة ، ظهرت ترجمة لمقال حول 20 طريقة من هذا القبيل على حبري ، لكن هذه القائمة ليست شاملة. أريد أن أتحدث عن طريقة أخرى من هذا القبيل تتعلق بتهيئة مثيلات std::thread
في std::thread
الدراسية.
هنا مثال بسيط على استخدام std::thread
:
class Usage { public: Usage() : th_([this](){ run(); }) {} void run() { // Run in thread } private: std::thread th_; };
في هذا المثال الأبسط ، يبدو الرمز صحيحًا ، ولكن هناك فضولًا واحدًا: في وقت استدعاء المنشئ std::thread
لم يتم بناء مثيل فئة Usage بالكامل. وبالتالي ، يمكن استدعاء Usage::run()
للحصول على مثيل ، بعض الحقول التي (لم يعلن عنها بعد الحقل std::thread
) لم تتم تهيئتها بعد ، والتي بدورها يمكن أن تؤدي إلى UB. يمكن أن يكون هذا واضحًا تمامًا في مثال صغير حيث يلائم رمز الفصل على الشاشة ، لكن في المشروعات الحقيقية يمكن إخفاء هذا الفخ خلف بنية الميراث المتفرعة. لنقم بتعقيد مثال العرض التوضيحي قليلاً:
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_; };
للوهلة الأولى ، تبدو الشفرة طبيعية تمامًا ، علاوة على ذلك ، ستعمل دائمًا كما هو متوقع ... حتى تضاف النجوم حتى يتم BadUsage::run()
قبل تهيئة ptr_
. للتدليل على ذلك ، أضف تأخيرًا صغيرًا قبل التهيئة:
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_; };
في هذه الحالة ، يؤدي استدعاء BadUsage::run()
إلى حدوث خطأ Segmentation ، ويشتكي valgrind من الوصول إلى ذاكرة غير مهيأة.
لتجنب مثل هذه الحالات ، هناك العديد من الحلول. الخيار الأسهل هو استخدام التهيئة ثنائية الطور:
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(); // ...