データの保護
C++0xのスレッドライブラリでは、多くのスレッド処理APIと同様、共有データを保護するための基本機能としてミューテックスを使用します。C++0xのミューテックスには次の4種類があります。
- 非再帰的ミューテックス(
std::mutex) - 再帰的ミューテックス(
std::recursive_mutex) - ロック関数でのタイムアウトが可能な非再帰的ミューテックス(
std::timed_mutex) - ロック関数でのタイムアウトが可能な再帰的ミューテックス(
std::recursive_timed_mutex)
どの種類のミューテックスでも、1つのスレッドを排他的に所有できます。非再帰的ミューテックスの場合は、同じスレッドから2回続けて(途中で解放せずに)ロックしたときの動作は未定義です。一方、再帰的ミューテックスの場合は、ロックカウントが増えるだけです。その場合、ロックしたのと同じ回数だけ解放しないと、他のスレッドがそのミューテックスをロックできません。
4種類のミューテックスには、ロックと解放を行うためのメンバ関数がそれぞれ用意されています。しかし大抵の場合は、std::unique_lock<>やstd::lock_guard<>というロッククラステンプレートを使うことをお勧めします。これらのクラスは、コンストラクタでミューテックスをロックし、デストラクタで解放するようになっています。これらをローカル変数として使用すれば、スコープを抜けるときにミューテックスのロックは自動的に解放されます。
std::mutex m;
my_class data;
void foo()
{
std::lock_guard<std::mutex> lk(m);
process(data);
} // mutex unlocked here
std::lock_guardはあえて基本機能のみとなっており、上記のような使い方だけが可能です。一方、std::unique_lockには、遅延ロック(deferred locking)、ロックの試行、タイムアウト付きのロックの試行、オブジェクトの破棄前のロック解放などの機能があります。ロックのタイムアウト機能が目的でstd::timed_mutexを利用する場合は、大抵はstd::unique_lockを使うことになります。
std::timed_mutex m;
my_class data;
void foo()
{
std::unique_lock<std::timed_mutex>
lk(m,std::chrono::milliseconds(3)); // wait up to 3ms
if(lk) // if we got the lock, access the data
process(data);
} // mutex unlocked here
これら2つのロッククラスはテンプレートなので、標準のミューテックス型すべてに対して利用できるほか、lock()関数とunlock()関数を持つ他の型にも利用できます。
複数のミューテックスをロックするときのデッドロックを防ぐ機能
場合によっては、複数のミューテックスをロックする処理が必要になることがあります。このとき一歩間違うと、忌まわしいデッドロックが発生しかねません。デッドロックとは、2つのスレッドが、2つの同じミューテックスを互いに逆の順序でロックしようとしたために、それぞれ1つをロックした状態で相手の終了を待ち続けるという状況に陥ることです。
C++0xのスレッドライブラリにはこの問題への対策として、複数のロックを一度に要求したい場合のために、複数のミューテックスをまとめてロックできるstd::lockというジェネリック関数が用意されています。各ミューテックスに対してメンバ関数lock()を順番に呼び出すのではなく、全部まとめてstd::lock()に渡すことで、デッドロックを心配せずにすべてをロックできるという機能です。この関数には、ロック前のstd::unique_lock<>のインスタンスを渡すこともできます。
struct X
{
std::mutex m;
int a;
std::string b;
};
void foo(X& a,X& b)
{
std::unique_lock<std::mutex> lock_a(a.m,std::defer_lock);
std::unique_lock<std::mutex> lock_b(b.m,std::defer_lock);
std::lock(lock_a,lock_b);
// do something with the internals of a and b
}
上の例で、仮にstd::lockを使わなかった場合、デッドロックが生じる可能性があります。例えば、X型の2つのオブジェクトxとyに対し、一方のスレッドがfoo(x,y)、もう一方がfoo(y,x)を呼び出した場合です。std::lockを使っていれば、その心配はありません。
初期化時のデータの保護
データを初期化する際にのみ保護が必要な場合には、ミューテックスではうまくいきません。初期化の完了後も無駄な同期が行われてしまうからです。C++0xの標準には、これに対処する方法がいくつか用意されています。
1つ目は、コンストラクタをC++0xの新しいconstexprキーワードを使って宣言し、さらにこのコンストラクタで定数の初期化の要件を満たすことです。この場合、コンストラクタで初期化される静的ストレージ期間のオブジェクトは、コードの実行に進む前に、静的初期化フェーズの一環として確実に初期化されることが保証されます。これはstd::mutexで利用する方法です。これにより、グローバルスコープでのミューテックスの初期化と競合する可能性を回避できます。
class my_class
{
int i;
public:
constexpr my_class():i(0){}
my_class(int i_):i(i_){}
void do_stuff();
};
my_class x; // static initialization with constexpr constructor
int foo();
my_class y(42+foo()); // dynamic initialization
void f()
{
y.do_stuff(); // is y initialized?
}
2つ目は、ブロックスコープで静的変数を使用することです。C++0xでは、ブロックスコープの静的変数の初期化は、関数の最初の呼び出し時に行われます。初期化の完了前に別のスレッドが同じ関数を呼び出した場合、2番目のスレッドは待機しなければなりません。
void bar()
{
static my_class z(42+foo()); // initialization is thread-safe
z.do_stuff();
}
どちらの方法も使えない場合(オブジェクトを動的に生成する場合など)は、std::call_onceとstd::once_flagを使うのが最適です。std::call_onceをstd::once_flag型の特定のインスタンスと組み合わせて使用すると、call_onceの名が示すとおり、指定した関数は1回のみ呼び出されます。
my_class* p=0;
std::once_flag p_flag;
void create_instance()
{
p=new my_class(42+foo());
}
void baz()
{
std::call_once(p_flag,create_instance);
p->do_stuff();
}
std::threadのコンストラクタと同様に、std::call_onceは関数の代わりに関数オブジェクトを受け取ることができ、複数の引数を取ることもできます。引数はデフォルトではコピーされ、std::refでラップすると参照渡しになるという点も同じです。
