yohhoyの日記

技術的メモをしていきたい日記

condition_variable_any+recursive_mutexの注意点

C++11標準ライブラリやBoost.Threadライブラリで提供されるcondition_variable_anyrecursive_mutexの組み合わせについてメモ。

まとめcondition_variable_anyrecursive_mutexの組み合わせ利用は避けること。*1 *2

条件変数condition_variable_anywaitメンバ関数仕様では、指定されたロックオブジェクトに対して1度だけunlock操作を行う。同関数に対して2回以上lock操作されている(ロックカウント > 1)再帰ロックオブジェクトを指定した場合、該当ロックは解放されない(ロックカウント != 0)まま関数を呼び出したスレッドAはブロック状態に遷移する。この状況では別スレッドBでのロック獲得要求もブロックされるため、プログラム全体としてはデッドロックに陥る。

condition_variable_any cv;
recursive_mutex mtx;
typedef unique_lock<recursive_mutex> Lock;

// スレッドA
//  角括弧[]はロックカウント遷移を表す
void threadA_waiter()
{
  Lock lk1(mtx);     // (1) 1回目のロック獲得[0→1]
  {
    //...
    Lock lk2(mtx);   // (2) 2回目のロック獲得[1→2]
    while (/*...*/) {
      cv.wait(lk2);  // (3) 1回だけロック解放[2→1]
    }
  }
}

// スレッドB
void threadB_signal()
{
  Lock lk(mtx);   // (4) 別スレッドからロック獲得不可!!
  cv.notify_one();
}

N2447時点では明示的な事前条件が設定(下線部)されていたが、その後のWording整理により明記されなくなった*3

void wait(Lock& lock);
Precondition: lock is locked by the current thread of execution. If Lock supports recursive locking, the lock count is one. No other thread of execution is waiting on this condition_variable_any object unless lock is, or refers to, the same underlying mutex object.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2447.htm

N3337 30.5.2/p10より引用。

template <class Lock> void wait(Lock& lock);
10 Effects:

  • Atomically calls lock.unlock() and blocks on *this.
  • When unblocked, calls lock.lock() (possibly blocking on the lock) and returns.
  • The function will unblock when signaled by a call to notify_one(), a call to notify_all(), or spuriously.
  • If the function exits via an exception, lock.lock() shall be called prior to exiting the function scope.

Boost.Thread 1.50.0ドキュメントより引用。

template<typename lock_type> void wait(lock_type& lock)
Effects:
Atomically call lock.unlock() and blocks the current thread. The thread will unblock when notified by a call to this->notify_one() or this->notify_all(), or spuriously. When the thread is unblocked (for whatever reason), the lock is reacquired by invoking lock.lock() before the call to wait returns. The lock is also reacquired by invoking lock.lock() if the function exits with an exception.

関連URL

*1:仕様的には waitメンバ関数び呼び出し時にロックカウントが 1 であればプログラマの意図通り動作する。一方で多重ロック獲得操作を前提として recursive_mutex を利用しているはずであり、wait関数呼び出し前にロックカウント==1を保証可能ならば設計段階から通常 mutex の利用を検討すべきだろう。一般に condition_variable+mutex の組み合わせに対しては、condition_variable_any+任意ロックオブジェクトよりも効率的な実装が提供される(N3337 30.5/p1)。

*2:C++11標準ライブラリ範囲内で condition_variable_any を用いる合理的な理由として、timed_mutex との組み合わせ利用が挙げられる。

*3:N2497には "Condition variable wording for wait/timed_wait clarified." とのみあり詳細経緯は不明。