boost

【C++】boost::thread使い方メモ

boostの中でも特によく使うthread周りの色々をまとめておきます。

1.基本形

1-A. サンプルコード

引数も返り値もなしでただ別スレッドを立ち上げるだけならboostはとても簡単なのです。

1-B. コンパイル例

上のコードをコンパイルする例です。
・CenOS7でboostをyumでインストールした場合
$ g++ -std=c++11 ./boost_thread.cpp -o ./boost_thread -lpthread -lboost_thread

・Ubuntu 18.04.3でboostをapt-getでインストールした場合
$ g++ -std=c++11 ./boost_thread.cpp -o ./boost_thread -lpthread -lboost_system -lboost_thread

・Macでboostをhomebrewを使用してインストールした場合
$ g++ -std=c++11 ./boost_thread.cpp -o ./boost_thread -lpthread -lboost_thread-mt

・自分でソースからインストールした場合
わざわざ古いバージョンをインストールした等でなければUbuntuの例と同じで問題ないです。
パスを通していない場合は-Iでboostのヘッダのディレクトリを、-Lでboostのライブラリのディレクトリを指定してください。

実行すると以下の出力が得られ、hogeスレッドとpiyoスレッドが並列で動作していることが確認できるかと思います。

[mathkuro@cent7 work]$ ./boost_thread
hogehoge
piyopiyo
hogehoge
piyopiyo
hogehoge
piyopiyo
hogehoge
piyopiyo
hogehoge
piyopiyo

2.引数を渡す場合

引数を渡す方法はいくつかありますが、ここではboost::bind()を使用する方法を紹介します。

boost::bind(func, arg1, arg2,…)で引数つき関数をスレッドにできていることが分かります。
ただし、この時、引数オブジェクトはコピーされるていることに注意してください。
(「子スレッド内で引数を操作したのに親スレッドで参照したら値が元のままだった」は大体これが原因)

コンパイル・出力内容は1.と同じなので省略します。

3.返り値(戻り値)を受け取る場合

返り値を受け取る場合はもう少し複雑です。

予め、boost::packaged_task<T>を作成しておき、それを用いてスレッドを立てる必要があります。

この時、返り値はpackaged_taskのget_future().get()で取得することができます。

4.クラスのメンバ関数でスレッドを立てる場合

クラスのメンバ関数でスレッドを立てる方が、2.とか3.の引数とか返り値で実装するよりも汎用性高いからいいですよね。

boost::threadのコンストラクタの第1引数にメンバ関数ポインタ、第2引数にクラスオブジェクトのポインタを指定するだけなのでお手軽です。

排他制御とか条件変数の実装とかを考えると、クラス化した方が楽な(見通しがいい)場合が多いように思います。

5.排他制御(mutex)

5-A. 基本形

boostには排他制御の色々も用意されています。

その中でも一番よく使うのがboost:mutexboost::lock_guardの組み合わせでしょう。

これにより範囲ロック(スコープドロック)ができ、スコープ({}のこと)を抜けると自動でロックが解除されるので、ロックの解除忘れを防ぐことができます。

糞みたいな例で申し訳ないのですが、上のコード場合は、排他制御なしで実行すると、スレッド1とスレッド2が同じ値を”取得→更新→反映”としてしまい、どちらかの更新が無かったことにされてしまいます。
こういう場合は、排他制御の出番ですね!(無理やり感)

ちなみに、boost::unique_lockもboost::lock_guardと同じく、範囲ロックをとることができます。が、違いはよく分かりません。。。

5-B. recursive-mutex

再帰的に呼び出される処理や、階層構造になっている処理の場合、同じスレッドで2回排他制御が呼ばれてデッドロックになってしまう、なんてこともあるかもしれません。

そういった場合は、 recursive-mutex で回避することができます。

ただし、 recursive-mutex は通常のmutexよりも処理コストが高いので必要のない場合は通常のmutexを使用しましょう。

5-C. read-lock/write-lock(shared-lock/upgrade-lock)

殆どのスレッドは参照するだけで、更新は極稀にしか起こらない、という状況であればread-lock/write-lockパターンを用いると、性能向上が見込めるかもしれません。

boostでのread-lock/write-lockは、shared-lock/upgrade-lock/unique-lockの三つで構成されており、以下のような関係になっています(通常の read-lock/write-lock パターンと大差はないと思いますが)。

  • shared-lockはread-lockのこと。同時に複数のスレッドが取得することができる。
  • upgrade-lockはwrite-lockになることができるロックのこと。同時に複数のスレッドが取得することはできない。
  • unique-lockはwrite-lockのこと。当然、 同時に複数のスレッドが取得することはできない。
  • upgrade-lock1つ、shared-lock複数は可能。
  • upgrade-lockがunique-lockにアップグレードできるのは、read-lockが0の時のみ。
  • upgrade-lock→unique-lock→upgrade-lockの遷移は可能
  • upgrade-lock→shared-lockの遷移は不可(ダウングレード??)。
  • shared-lockはアップグレードできない。

こういうのは実際に動かしてみた方が分かりやすいです。
↓サンプルプログラム

これを実行すると、以下のような結果となり、read-lock/write-lockパターンとして動作していることが分かります。

read thread: 0: start. ←read-lockは同時に複数のスレッドが取得可能
read thread: 1: start.
read thread: 2: start.
read thread: 3: start.
read thread: 4: start.
write thread: 0: start. ←read-lockが取られている際にもupgrade-lockは取得可能
read thread: 0: end.
read thread: 1: end.
read thread: 2: end.
read thread: 3: end.
read thread: 4: end.
write thread: 0: lock upgraded. ←すべてのread-lockが解放されたらupgrade可能。
write thread: 0: end.
write thread: 3: start. ←upgrade-lockをとることができるのは同時に一つだけ。
write thread: 3: lock upgraded.
write thread: 3: end.
write thread: 1: start.
write thread: 1: lock upgraded.
write thread: 1: end.
write thread: 2: start.
write thread: 2: lock upgraded.
write thread: 2: end.
write thread: 4: start.
write thread: 4: lock upgraded.
write thread: 4: end.

6.条件変数(condition_variable)

mutexほどよく使うわけではありませんが、lockだけだと表現するのが難しいスレッド間の制御を実装したい場合にたまに使いますよね条件変数。

これを条件変数無しで実装しようとすると、cond_.wait()のところをsleepにしてwhile回し続けることになりますが、そんな実装はダサすぎるのでやめてくださいね。

7.エラー等の対処・TIPS

7-A. BOOST_THREAD_MOVABLE_ONLYでコンパイルエラー

直訳すると、「スレッドをコピーすんなやボケ」になります。

boost::threadオブジェクトはムーブのみ(コピー不可)の制約があるので 、
↓みたいなコードを書くと怒られます。

std::list<boost::thread> thread_list;

for (int i = 0; i < LOOP_NUM; ++i) {
    boost::thread th(Hoge);
    thread_list.push_back(th);
}

この場合、thread_list.push_back(th); の部分でコピーが発生しています。
渡し方を変えてやる、リストにしない等の対応をしましょう。

修正版(一例)

std::list<boost::thread *> thread_list;

for (int i = 0; i < LOOP_NUM; ++i) {
    boost::thread *th = new boost::thread(Hoge);
    thread_list.push_back(th);
}

7-B. boost::condition_variableでアボート

/usr/local/include/boost/thread/pthread/condition_variable_fwd.hpp:81: boost::condition_variable::~condition_variable(): Assertion `!posix::pthread_mutex_destroy(&internal_mutex)’ failed.
Aborted

↑のようなエラーが出た場合は、join()の失敗が原因です。

join()前にboost::threadのデストラクタが走ってしまっていないか確認してください。

スレッドを起動したスコープ{}の外側でjoin()して、abortしてしまう場合が多いです。

7-C. undefined reference to symbol ‘_ZN5boost6system15system_categoryEv’でコンパイルに失敗

undefined reference to symbol ‘_ZN5boost6system15system_categoryEv’
//usr/lib/x86_64-linux-gnu/libboost_system.so.1.65.1: error adding symbols: DSO missing from command line
collect2: error: ld returned 1 exit status

↑のようなエラーが出た場合は、コンパイルに必要な依存性の解決ができていないことが原因です。

コンパイルオプションに-lboost_systemを入れ忘れていないか、間違って-lboost-systemとかになっていないかを確認しましょう。

https://www.boost.org/doc/libs/1_71_0/doc/html/thread/build.html
公式の手順に書いてある通り、-lboost_systemは必須で、ユーザがコンパイル時に指定する必要があります。

参考

https://boostjp.github.io/tips/thread.html
http://rabbitfoot530.hatenablog.com/entry/20120415/1334474779

コメント

タイトルとURLをコピーしました