c++11 からマルチスレッドに関する標準ライブラリが本格的に整備されました。
マルチスレッドセーフなクラスを実装する方法は様々ですが、
「とりあえずこれだけ抑えておけ」的なクラス宣言サンプルを紹介します。
#include <mutex>
// 任意のスレッドから new / delete / 全てのメンバ関数 の呼び出しが可能な
// スレッドセーフ・クラスの実装サンプル
class SomeClass
{
int Value;
int Cnt;
/*
std::recursive_mutexクラスは
std::mutex とは違って
同一スレッドからの再帰的なロック取得を許可します。
*/
std::recursive_mutex _mutex;
inline std::unique_lock<std::recursive_mutex> Locker()
{
return std::unique_lock<std::recursive_mutex>( _mutex );
}
public:
int IncCnt()
{
std::unique_lock<std::recursive_mutex> locker = Locker();
++Cnt;
return Cnt;
}
int GetValue()
{
std::unique_lock<std::recursive_mutex> locker = Locker();
IncCnt();
return Value;
}
void SetValue( int v )
{
std::unique_lock<std::recursive_mutex> locker = Locker();
IncCnt();
Value = v;
}
SomeClass()
{
// コンストラクタでの排他制御操作は不要です。
// コンストラクタで構築中のオブジェクトには、
// 他スレッドから並行アクセスする手段が存在しないため、
// コンストラクタ内処理が非staticメンバ変数への
// アクセスのみならば排他制御の必然性がありません。
Cnt = 0;
SetValue(0);
}
};
unique_lockテンプレートクラスは、std::mutex の lock() / unlock() を
コンストトラクタとデストラクタで確実に呼び出すための標準クラスです。
このサンプルではstd::mutexではなく、std::recursive_mutex を使用しています。
std::recursive_mutex は異なるスレッド間で使用するクラスメンバ変数の
Read/Writeを排他制御するためのクラスです。
std::mutexとの違いは、「同一スレッドで既にロックされている場合、そのまま通過させる」
という点です。
なぜ std::mutex を使わなかったのでしょうか?
ここでは、SomeClass::SetValue()を見てみましょう。
void SetValue( int v )
{
std::unique_lock<std::recursive_mutex> locker = Locker();
IncCnt();
Value = v;
}
SetValue()突入直後に recursive_mutex でlockしています。
自身のメンバ関数 IncCnt() を呼び出し後、
Valueの値をSetしています。
int IncCnt()
{
std::unique_lock<std::recursive_mutex> locker = Locker();
++Cnt;
return Cnt;
}
::IncCnt() はユーザに公開するpublicメンバ関数のため
当然、::SetValue() の突入直後も Locker() でロックしてます。
もし std::mutex を使っていた場合、既にロックされているため
ここで無限待機に陥ってしまいます。
また「正常無限待機」のため、デバッガなどでも捕捉しにくいです。
「複数スレッド間 排他制御だけをしたい」という当初の目的を達成したい場合、
std::recursive_mutex のほうが使い勝手が良いのです。
ただし、「同一スレッドチェック」が挿入されている分、
std::mutex よりも若干のオーバーヘッドがあります。
新規に、スレッドセーフなクラスを確実に手っ取り早く作りたい場合は
とりあえずstd::recursive_mutex を使用しておいて、
どうしても最適化が必要な場合のみ、後からstd::mutex に切り替えるといった
手法が無難かもしれません。