マルチスレッドプログラムで printf デバッグをする際に、出力文字列が混ざらないよう排他制御を行うコード記述方法に関するメモ。
本記事では排他制御に関するテクニックに限定し、保守性よりも記述の手軽さを重要視している。(Boost.Formatなどログ出力部に関する話題はスコープ外。)
もっとも単純な方式
普通にmutex+lock_guardでログ出力コードを保護する。lock_guard
オブジェクト生成とRAIIイディオム用のブロック { }
が必要となり、ログ出力部以外のコード記述が煩雑になる。(もちろん std::lock_guard<std::mutex>
型をtypedefすれば多少は短くなる。)
#include <iostream> #include <mutex> std::mutex cout_mutex; // std::cout保護用mutex // 並行処理で呼び出される関数 void parallel_invoked() { const std::thread::id tid = std::this_thread::get_id(); int x, y; // ... { // printfデバッグ用にスレッドIDと変数x, yをダンプ std::lock_guard<std::mutex> lk(cout_mutex); std::cout << tid << ":" << x << "," << y << std::endl; } // ... }
インラインロック方式
Boost.勉強会 #8 大阪での[twitter:@wraith13]さんの発表C++ tips 3 カンマ演算子編を見て思いついた方法。lock/unlockを行う一時オブジェクトとカンマ演算子を組み合わせ、1行でlock+ログ出力+unlock処理を行っている。行頭に「autolk(),
」を記載するだけなので簡潔なコードとなる。
std::unique_lock<std::mutex> autolk() { // cout_mutexロック済みのunique_lockを(ムーブで)返す return std::unique_lock<std::mutex>(cout_mutex); } void parallel_invoked() { const std::thread::id tid = std::this_thread::get_id(); int x, y; // ... autolk(), std::cout << tid << ":" << x << "," << y << std::endl; // ... }
ロックオブジェクト(unique_lock
)の生成に autolk
関数を経由するのは次の理由による。
typedef std::unique_lock<std::mutex> Lock; Lock(cout_mutex), cout << /*...*/ << endl; // [A] ill-formed (Lock(cout_mutex)), cout << /*...*/ << endl; // [B] OK
最初に思いつくであろうコード[A]では、「Lock型の変数cout_mutexの宣言、変数coutの宣言、続く不正な<<演算子」と解釈されてコンパイルエラーになる。本来の意図通り「引数にcout_mutexをとるLock型の一時オブジェクトの生成」と解釈させるには、コード[B]のように括弧でくくる必要がある。この括弧を毎回忘れずに記述し、かつcout_mutexを指定するのも面倒なため、ロックオブジェクト生成(=lock取得)を関数経由で行っている。
ラムダ方式(おまけ)
C++11ラムダ式を使った記述方法も考えたが、デバッグ用出力コード以外の部分(dump([&]{ ... });
)が冗長でさほどメリットを感じない。強いて利点を挙げれば、dump
関数側を変更してログ出力処理を一括制御できることか。
template <class F> void dump(F f) { std::lock_guard<std::mutex> lk(cout_mutex); f(); } void parallel_invoked() { const std::thread::id tid = std::this_thread::get_id(); int x, y; // ... dump([&]{ std::cout << tid << ":" << x << "," << y << std::endl; }); // ... }