C++11標準ライブラリのミューテックスクラスと、unlock操作〜オブジェクト破棄で起こり得る挙動についてメモ。
注意:ミューテックスクラスの内部実装に対する要件の話であり、標準ライブラリ利用者にとっては “意図通り” 動作することが保証されている。(言い換えると、ミューテックスを自作する場合には考慮すべき事項)
スレッドセーフな参照カウンタを内包するクラスX
を考える。ミューテックスX::m
は参照カウンタ変数X::c
の保護用。また関数release_object
では参照カウンタを操作し、参照カウントが 0 となった時にオブジェクトを破棄する。(サンプルコード片はN3018より引用)
struct X { //... std::mutex m; // X::cアクセス保護 int c; // 参照カウンタ }; void release_object(X *o) { o->m.lock(); // (1) bool del = (--(o->c) == 0); // (2) o->m.unlock(); // (3) if (del) { delete o; } // (4) }
2つのスレッドA, Bのみが同一X
オブジェクトの所有権を保持しており(X::c==2
)、同時にrelease_object
関数を呼び出す。ここでは下記順序にて処理が行われたとする。
- A:(1)
m.lock
メンバ関数呼び出しによるロック獲得。 - B:(1)
m.lock
メンバ関数呼び出し。ロック獲得失敗のためブロックされる。 - A:(2) 参照カウンタをデクリメントし(2→1)、変数
del
にfalseを代入。 - A:(3)
m.lock
メンバ関数呼び出しによるロック解放。ただしスレッドAはまだm.lock
メンバ関数内を処理中とする。 - B:(1) ロック獲得に成功し(Step2の続き)、
m.lock
メンバ関数呼び出しからreturn。 - B:(2) 参照カウンタをデクリメントし(1→0)、変数
del
にtrueを代入。 - B:(3)
m.lock
メンバ関数呼び出しによるロック解放。 - B:(4) 変数
del
がtrueのため、オブジェクトo
のデストラクト処理。メンバ変数o::m
も破棄される。 - A:(3)
m.lock
メンバ関数内の残り処理(Step4の続き)を実行し、関数呼び出しからreturn。このとき既にミューテックスm
は破棄済み! - A:(4) 変数
del
はfalseのため何もしない。
C++11標準規格は、上記シナリオでも正常動作することを処理系に要求する(30.4.1.2.1/p2)。
N3337 30.4.1.2/p5(一部), 30.4.1.2.1/p2より引用。
5 (snip) [Note: Construction and destruction of an object of a mutex type need not be thread-safe; other synchronization should be used to ensure that mutex objects are initialized and visible to other threads. -- end note]
2 [Note: After a thread
A
has calledunlock()
, releasing a mutex, it is possible for another threadB
to lock the same mutex, observe that it is no longer in use, unlock it, and destroy it, before threadA
appears to have returned from its unlock call. Implementations are required to handle such scenarios correctly, as long as threadA
doesn't access the mutex after the unlock call returns. These cases typically occur when a reference-counted object contains a mutex that is used to protect the reference count. -- end note]
関連URL
- N3018 C++ Standard Library Active Issues List (Revision R69), 1218. mutex destructor synchronization