C++ で Read-Write lock パターン

同じデーターに対して複数のスレッドが入り乱れて読み書きを行う場合、排他のオーバーヘッドが無視できずに問題になることがあります.

そこで Read アクセスのみの場合は排他を行わなわず、Read-Write がかち合った場合にのみ排他を行うことでオーバーヘッドの低減を図ろうというのがこのパターンです.

Read-Write lock パターンは shared mutex という形で標準ライブラリで提供されています (shared mutex の詳細については以下のリンクを参照下さい)

cppreference

C++ でマルチスレッドデザインパターン

C++ で Thread-Specific Storage パターン

複数のスレッドから読み書きされるデータはその内容の整合性を保つため、基本的には排他処理を行う必要があります(Single Thread Execution パターン

Thread-Specific Storage パターンは同じデータに複数スレッドからアクセスが行われ、かつスレッド間でのデータの共有が不要といったケースにおいて、スレッド毎に専用の領域を用意する事で排他を行わずに並列実行可能なようにすると言うパターンです.

C++11 から、新たな記憶クラス指定子として thread_local というものが用意されており、これを使うと簡単に Thread-Specific Storage パターンを使うことができます (thread_local 記憶クラス指定子についての詳細は以下のリンクを参照下さい)

cppreference

このパターンの実際の使い所ですが、積極的に使用するケースというのはあまりなく、「シングルスレッドでの使用を前提に静的領域を多用している(場合によってはレガシーな)コードを比較的簡単にマルチスレッド化する」といったような用途で使用される事が多いような気がします.

C++ でマルチスレッドデザインパターン

C++ で Thread-Per-Message パターン

Thread-Per-Mesasge パターンとは時間を要する処理要求に対し、一つ一つスレッドを割り当てる事で応答速度の向上を目的とするパターンです.

例えば以下は時間のかかるファイル保存の処理に Thread-Per-Message を適用した例です.

ここではダミーで標準出力へ出すようにしていますが、 Helper は処理の本体であるファイル保存を行うクラスで Host は client からの要求を受け、処理を行うスレッドを起動するクラスです.  スレッドはインスタンス化と同時に detach され、Host とは完全非同期に処理を実行します.(上記のような簡単な例だと Helper を利用するのが大げさに感じてしまいますが、本来のパターンに忠実に従い上では Helper を定義しています)

さてこの Thread-Per-Message パターンですが、実は使い所が大変難しいパターンだと思います.  なぜ難しいかと言うと スレッドは生成と同時に detach されて生成元のあずかり知らないところで動作を行うため、生成元で処理の終了やエラーの発生を感知できないからです.

では逆にどんな処理なら問題無いかと言うと

(1) 絶対エラーが発生しない (もしくは発生しても問題ないと) と分かっている単純な処理.
(2) deadlock 等が発生せず、一定の時間で終了することが保証されている処理.
(3) スレッドで実行される処理が非同期に動作するプログラムとして高い独立性を持っており、エラー処理等を完全に自己で完結出来る場合.

の場合のどれかに当てはまる場合でしょう.  (1) は言うまでも無いと思いますが、(2) の場合 deadlock を起こしたスレッドが開放されない事で徐々に OS のリソースを食いつぶして行き、最終的に処理速度の低下やスレッド生成の失敗等につながる恐れがあります.  (3) は Apache 等の Web サーバーがリクエストを捌く際に行う処理がまさにそれです (Apache の場合はリクエスト毎にスレッドでは無くプロセスを起動するので Process-Per-Message というのが正しいですが..)

というわけで今回取り上げたファイルに保存すると言う例も、ログファイルなどの「まぁ、問題が起こったら保存されなくてもしょうがないよね」というような用途以外で使用するのは適切ではありませんのでご注意を.

C++ でマルチスレッドデザインパターン

 

C++ で Balking パターン

Balking パターンとは「オブジェクトに対しての処理要求に対し、オブジェクトが特定の状態にある場合にのみ処理を行う(そうでない場合は何もしないで抜ける)」というパターンです。

例えば GUI システムでは多くの場合 main スレッドが UI のイベントループをハンドルしているため、lock 待ち等で main スレッドが待ち状態に入ると UI がフリーズしてしまいユーザビリティーの観点から好ましくありません。その場合 try lock のような仕組みを利用して確実に lock が取得できる場合のみ lock 取得処理を行う(そうでない場合はイベントループを回し、lock が取得できるようになるまで待つ)と言うことが良く行われます.

上記がここで取り上げた例の擬似コードになります.  パターンの適用例としてはあまり紹介されていないように思いますが、これも一種の Balking パターンだと思います.

C++ でマルチスレッドデザインパターン

C++ std::regex_token_iterator で文字列を split (token 分解)

完全に boost ::tokenizer 相当と言うわけでは無いですが、C++11 から追加された正規表現ライブラリに std::regex_token_iterator  と言うイテレータが含まれており、これを文字列の split に利用する事ができます。

std::regex_token_iterator (cppreference.com)

詳細については上記のリンクを見ていただくとして、std::regex_token_iterator の基本的な使い方は以下のようになります。

上記の例を見て頂ければ分かると思いますが、コンストラクタの第4引数に正規表現でキャプチャした文字列のうちどのインデックスのものをイテレーション対象にするかを指定します。インデックスには 0 と -1 を指定することが可能で、0 を指定した場合はマッチした文字列全体が、-1 を指定すると対象の文字列全体からマッチした部分を取り除いた断片がイテレーションの対象になります。

この「マッチした部分を取り除いた断片をイテレーションする」と言う機能を利用すると split 処理を書く事ができ、例えば

のようになります。

ただ処理速度的にはアレだと思いますので、速度が要求される場面では従来から行われている愚直な処理の方がマッチするのではないかと思います。

 

C++ で Future パターン

Future とはスレッドで実行している処理との同期、スレッドからの結果の受領を行うためのパターンです。Future パターンは C++11 から std::future という形で標準ライブラリで提供されていますので、具体例については  cppreference.com  などをご参照下さい。

C++ でマルチスレッドデザインパターン

 

C++ で Worker Thread パターン

Worker Thread とは一般に生成、破棄コストの高いスレッドをプールして使い回すためのパターンで Thread Pool とも呼ばれます。 Worker Thread パターンは Producer-Consumter パターンに基づいており、キューで受け渡されるのがデータではなく実行可能な仕事を表現したオブジェクトである点が特徴です。

以下の例で Runnable は仕事の基底クラスであり、std::shared_ptr<Runnable> のキューをを介してプールされているスレッドに仕事を依頼します。 クライアントは Runnable のサブクラスを作成し、インスタンスをスレッドプールに add する事で自分でスレッドをハンドルする事無く非同期に仕事を実行させる事ができます。

実際に使用してみると以下のようになります。 以下の例では WorkA, WorkB という異なる2つの内容の Runnable サブクラスのインスタンスを add しています。 仕事は Runnable で抽象化されていますので WorkA、 WorkB 以外にも任意の仕事を実行させる事が可能です。

Runnable のような基底クラスではなく特定の仕事クラスのキューを持たせる事も可能ですが、より汎用的にするためにこのようにするケースが多いように思います。また C++11 からは std::function が利用できますので、std::function のキューにするとより C++ っぽいかもしれません。

std::function 版の使用例です。

C++ でマルチスレッドデザインパターン