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に貼り付けたがらないことと、一般に受け入れられている代替手段がないことです。








All Articles