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
Ahas calledunlock(), releasing a mutex, it is possible for another threadBto lock the same mutex, observe that it is no longer in use, unlock it, and destroy it, before threadAappears to have returned from its unlock call. Implementations are required to handle such scenarios correctly, as long as threadAdoesn'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