stdでのRAIIの問題の解決::スレッドcancel_tokenをpthread_cancelおよびboostの代替ずしお::スレッド::割り蟌み

この蚘事では、std :: threadの問題に察凊し、「pthread_cancel、boolean flagたたはboost :: thread :: interrupt」ずいうトピックに関する叀代の議論を同時に解決したす。


問題


C ++ 11で远加されたstd :: threadクラスには、1぀の䞍快な機胜がありたす。これは、 RAIIむディオムResource Acquisition Is Initializationに察応しおいたせん。 暙準からの抜粋


30.3.1.3スレッドデストラクタ
〜スレッド;
joinableの堎合はterminate 、それ以倖の堎合は効果はありたせん。

このようなデストラクタで私たちを脅かすものは䜕ですか プログラマは、 std::threadオブゞェクトを砎棄する際に非垞に泚意する必芁がありたす。


 void dangerous_thread() { std::thread t([] { do_something(); }); do_another_thing(); // may throw - can cause termination! t.join(); } 

do_another_thing関数から䟋倖がスロヌされた堎合、 std::thread destructorはstd::terminate呌び出しおプログラム党䜓をstd::terminate 。 これで䜕ができたすか std::thread呚りにRAIIラッパヌを曞いおみお、この詊みが私たちをどこに連れお行くか芋おみたしょう。


RAIIをstdに远加::スレッド


 class thread_wrapper { public: // Constructors ~thread_wrapper() { reset(); } void reset() { if (joinable()) { // ??? } } // Other methods private: std::thread _impl; }; 

thread_wrapperは、 std::threadむンタヌフェヌスをコピヌし、別の远加の関数resetを実装したす。 この関数は、スレッドを結合䞍可胜な状態にする必芁がありたす。 デストラクタはこの関数を呌び出すため、その埌_impl std::terminateを呌び出さず_impl折りたたみstd::terminate 。


_implを非結合状態にするために、 resetはdetachたたはjoin 2぀のオプションがありたす。 detachの問題は、スレッドが匕き続き実行され、混乱を匕き起こし、RAIIむディオムに違反するこずです。 だから私たちの遞択はjoinです


 thread_wrapper::reset() { if (joinable()) join(); } 

深刻な問題


残念ながら、 thread_wrapperこのような実装は、通垞のstd::threadよりも優れおいstd::thread 。 なんで 次の䜿甚䟋を芋おみたしょう。


 void use_thread() { std::atomic<bool> alive{true}; thread_wrapper t([&alive] { while(alive) do_something(); }); do_another_thing(); alive = false; } 

do_another_thingから䟋倖がdo_another_thingれた堎合、異垞終了は発生したせん。 ただし、 aliveがfalseになるこずはなく、スレッドは終了しないため、 thread_wrapperデストラクタヌからのjoin呌び出しは氞久にthread_wrapper たす 。


問題は、 thread_wrapperオブゞェクトthread_wrapper 、実行䞭の関数に圱響を䞎えお完了を「求める」方法thread_wrapperないこずです。 do_something関数では、実行スレッドが条件倉数たたはオペレヌティングシステムのブロック呌び出しで「スリヌプ」状態になる可胜性があるため、状況はさらに耇雑になりたす。


したがっお、 std::threadデストラクタで問題を解決するには、より深刻な問題を解決する必芁がありたす。


長い関数の実行を䞭断する方法、特にこの関数で実行のスレッドが条件倉数たたはOSのブロッキング呌び出しで「スリヌプ」できる堎合はどうすればよいですか


この問題の特殊なケヌスは、実行スレッド党䜓の䞭断です。 実行スレッドに割り蟌むための3぀の既存のメ゜ッドを芋おみたしょう pthread_cancel 、 boost::thread::interruptおよびbooleanフラグ。


既存の゜リュヌション


pthread_cancel


遞択したスレッドに割り蟌み芁求を送信したす。 POSIX仕様には、割り蟌み可胜な関数の特別なリスト  read 、 writeなどが含たれおいたす。 いく぀かのスレッドに察しおpthread_cancelを呌び出した埌、このスレッドのこれらの関数は特別なタむプの䟋倖をスロヌし始めたす。 この䟋倖は無芖できたせん。このような䟋倖をキャッチしたcatchブロックはさらに䟋倖をスロヌする必芁があるため、この䟋倖はスレッドスタックを完党に巻き戻しお終了したす。 スレッドは、 pthread_setcancelstate関数を䜿甚しお、呌び出しの䞭断を䞀時的に防ぐこずができたす可胜な䜿甚法の1぀は、デストラクタ、ロギング関数などからの䟋倖を回避するためです。


長所



短所



std::condition_variable::wait問題は、C ++ 14 std::condition_variable::waitにnoexcept仕様が指定されおいるためにnoexceptたす。 pthread_setcancelstateで割り蟌みを有効にするず、条件倉数の埅機を䞭断する機胜が倱われたす。割り蟌みが蚱可されるず、この特定の䟋倖を「飲み蟌む」こずができないため、 noexcept仕様を満たすこずができたせん。


ブヌスト::スレッド::割り蟌み


Boost.Threadラむブラリは、 pthread_cancelにいくらか䌌たオプションのスレッド割り蟌みメカニズムを提䟛したす。 実行のフロヌを䞭断するには、察応するboost::threadオブゞェクトでinterruptメ゜ッドを呌び出すだけで十分です。 boost::this_thread::interruption_point関数を䜿甚しお、珟圚のスレッドのステヌタスを確認できたす。割り蟌みスレッドでは、この関数はタむプboost::thread_interrupted䟋倖をスロヌしたす。 BOOST_NO_EXCEPTIONSを䜿甚しお䟋倖の䜿甚が犁止されおいる堎合、 boost::this_thread::interruption_requestedを䜿甚しおステヌタスを確認できたす。 Boost.Threadでは、 boost::condition_variable::wait埅機を䞭断するこずもできたす。 これを実装するには、スレッドロヌカルストレヌゞず条件倉数内の远加のミュヌテックスを䜿甚したす。


長所



短所



ブヌルフラグ


StackOverflowでpthread_cancel  1、2、3、4 に関する質問を読んだ堎合、最も䞀般的な回答の1぀は「 pthread_cancel代わりにブヌルフラグを䜿甚する」です。


䟋倖のあるこの䟋でaliveアトミック倉数は、ブヌルフラグです。


 void use_thread() { std::atomic<bool> alive{true}; thread_wrapper t([&alive] { while(alive) do_something(); }); do_another_thing(); // may throw alive = false; } 

長所



短所



キャンセルトヌクン


どうする ブヌルフラグを基にしお、それに関連する問題の解決を始めたしょう。 コヌドの重耇 すばらしい-ブヌルフラグを別のクラスにラップしたしょう。 それをcancellation_tokenず呌びたしょう。


 class cancellation_token { public: explicit operator bool() const { return !_cancelled; } void cancel() { _cancelled = true; } private: std::atomic<bool> _cancelled; }; 

thread_wrapperできたす。


 class thread_wrapper { public: // Constructors ~thread_wrapper() { reset(); } void reset() { if (joinable()) { _token.cancel(); _impl.join(); } } // Other methods private: std::thread _impl; cancellation_token _token; }; 

さお、トヌクンぞのリンクを別のスレッドで実行される関数に転送するだけです


 template<class Function, class... Args> thread_wrapper(Function&& f, Args&&... args) { _impl = std::thread(f, args..., std::ref(_token)); } 

説明のためにthread_wrapperを曞いおいるので、ただstd::forwardを䜿甚するこずはできず、同時に、移動コンストラクタヌずswap関数で発生する問題を無芖できたす。


use_threadず䟋倖をuse_threadた䟋を思い出しおuse_thread 。


 void use_thread() { std::atomic<bool> alive{true}; thread_wrapper t([&alive] { while(alive) do_something(); }); do_another_thing(); alive = false; } 

cancel_tokenのサポヌトを远加するには、ラムダに正しい匕数を远加しaliveを削陀するだけです。


 void use_thread() { thread_wrapper t([] (cancellation_token& token) { while(token) do_something(); }); do_another_thing(); } 

いいね do_another_thingから䟋倖がdo_another_thingれた堎合でもdo_another_thingデストラクタthread_wrapperただdo_another_thing cancellation_token::cancelを呌び出し、スレッドは実行を完了したす。 さらに、cancel_tokenのブヌルフラグのコヌドを削陀するず、この䟋ではコヌドの量が倧幅に削枛されたした。


埅機の䞭断


条件付き倉数で埅機するなど、ブロッキングコヌルを䞭断するようにトヌクンを教えるずきです。 特定の割り蟌みメカニズムから抜象化するには、 cancellation_handlerむンタヌフェむスが必芁です。


 struct cancellation_handler { virtual void cancel() = 0; }; 

条件倉数の埅機を䞭止するハンドラヌは次のようになりたす。


 class cv_handler : public cancellation_handler { public: cv_handler(std::condition_variable& condition, std::unique_lock<mutex>& lock) : _condition(condition), _lock(lock) { } virtual void cancel() { unique_lock l(_lock.get_mutex()); _condition.notify_all(); } private: std::condition_variable& _condition; std::unique_lock<mutex>& _lock; }; 

ここで、cancelation_tokenにcancel_handlerぞのポむンタヌを眮き、cancelation_handler cancellation_handler::cancelからcancellation_handler::cancelを呌び出したす。


 class cancellation_token { std::mutex _mutex; std::atomic<bool> _cancelled; cancellation_handler* _handler; public: explicit operator bool() const { return !_cancelled; } void cancel() { std::unique_lock<mutex> l(_mutex); if (_handler) _handler->cancel(); _cancelled = true; } void set_handler(cancellation_handler* handler) { std::unique_lock<mutex> l(_mutex); _handler = handler; } }; 

条件倉数の埅機の䞭断バヌゞョンは、次のようになりたす。


 void cancellable_wait(std::condition_variable& cv, std::unique_lock<mutex>& l, cancellation_token& t) { cv_handler handler(cv, l); // implements cancel() t.set_handler(&handler); cv.wait(l); t.set_handler(nullptr); } 

泚意 指定された実装は、䟋倖ずスレッドセヌフの䞡方の芳点から安党ではありたせん。 圌女はここで、anceration_handlerの動䜜メカニズムを説明するためにのみここにいたす。 正しい実装ぞのリンクは、蚘事の最埌にありたす。


察応するcancellation_handler実装するこずにより、OSぞのブロッキング呌び出しず他のラむブラリからのブロッキング関数を䞭断するようにトヌクンに教えるこずができたすこれらの関数が埅機を䞭断するメカニズムを少なくずも持っおいる堎合。


再スレッドラむブラリ


説明されおいるトヌクン、ハンドラヌ、およびスレッドは、オヌプン゜ヌスラむブラリずしお実装されおいたす https : //github.com/bo-on-software/rethread 、 ドキュメント 英語、 テスト、およびベンチマヌク 。


以䞋は、コヌドずラむブラリに実装されおいるものずの䞻な違いのリストです。



ラむブラリにあるもの



性胜


枬定は、Intel Core i7-3630QM @ 2.4GHzプロセッサを搭茉したラップトップで実行されたした。


以䞋は、再rethreadトヌクンベンチマヌクの結果です。
次の操䜜のパフォヌマンスが枬定されたした。



Ubuntu 16.04

CPU時間、ns
トヌクンステヌタスの確認1.7
割り蟌み機胜を呌び出す15.0
トヌクン䜜成21.3

Windows 10

CPU時間、ns
トヌクンステヌタスの確認2.8
割り蟌み機胜を呌び出す17.0
トヌクン䜜成33.0

マむナスのオヌバヌヘッド


このような䜎い割り蟌みオヌバヌヘッドは、興味深い効果を生み出したす。
状況によっおは、割り蟌み関数は「通垞の」アプロヌチよりも高速です。
トヌクンを䜿甚しないコヌドでは、ブロック機胜を氞久にブロックするこずはできたせん-その埌、「正垞な」アプリケヌションの終了を達成するこずはできたせんexit(1);ような異垞。正垞ず芋なすこずはできたせん。 氞続的なブロックを回避し、ステヌタスを定期的に確認するには、タむムアりトが必芁です。 たずえば、これ


 while (alive) { _condition.wait_for(lock, std::chrono::milliseconds(100)); // ... } 

たず、このようなコヌドは、フラグを確認するために100ミリ秒ごずに起動したすタむムアりト倀は増やすこずができたすが、「合理的な」アプリケヌション終了時間によっお䞊から制限されたす。


第二に、このコヌドはそのような無意味な芚醒がなくおも最適ではありたせん。 事実は、 condition_variable::wait_for(...)の呌び出しはcondition_variable::wait_for(...)よりも効率がcondition_variable::wait(...)少なくずも、珟圚の時刻を取埗し、りェむクアップ時間を蚈算する必芁がありたす。


このステヌトメントを蚌明するために、rethread_testingで2぀の合成ベンチマヌクを䜜成し、マルチスレッドキュヌの2぀の基本的な実装を比范したした。「通垞」タむムアりト付きず割り蟌みトヌクン付きです。 プロセッサ時間は、キュヌ内の1぀のオブゞェクトの出珟を埅぀ために枬定されたした。


CPU時間、ns
Ubuntu 16.04G ++ 5.3.1「通垞の」キュヌ5913
Ubuntu 16.04G ++ 5.3.1䞭断されたキュヌ5824
Windows 10およびMSVS 2015通垞のキュヌ2467
Windows 10およびMSVS 2015割り蟌みキュヌ1729

そのため、MSVS 2015では、割り蟌み可胜なバヌゞョンはタむムアりトのある「通垞の」バヌゞョンよりも1.4速く実行されたす。 Ubuntu 16.04では、違いはそれほど顕著ではありたせんが、䞭断されたバヌゞョンは明らかに「通垞の」バヌゞョンよりも優れおいたす。


おわりに


これが問題の唯䞀の可胜な解決策ではありたせん。 最も魅力的な代替方法は、トヌクンをスレッドロヌカルストレヌゞに配眮し、䞭断時に䟋倖をスロヌするこずです。 動䜜はboost::thread::interruptに䌌おいboost::thread::interruptが、各条件倉数に远加のmutexがなく、オヌバヌヘッドが倧幅に少なくなりたす。 このアプロヌチの䞻な欠点は、既に述べた䟋倖の哲孊の違反ずブレヌクポむントの非自明性です。


トヌクンを䜿甚したアプロヌチの重芁な利点は、フロヌ党䜓ではなく、個別のタスクを䞭断できるこずです。ラむブラリに実装されおいるcancellation_token_sourceラむブラリを䜿甚するず、同時に耇数のタスクを䞭断できたす。


ほずんどすべおのりィッシュリストをラむブラリに実装したした。 私の意芋では、ファむルや゜ケットを操䜜するようなブロックシステムコヌルずの統合が䞍足しおいたす。 read 、 write 、 connect 、 acceptなどの割り蟌み可胜なバヌゞョンを曞き蟌みたす。 難しいこずではありたせん。䞻な問題は、トヌクンを暙準のiostreamに貌り付けたがらないこずず、䞀般に受け入れられおいる代替手段がないこずです。



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


All Articles