yohhoyの日記

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

タイムアウト付き待機関数とspurious wakeupの微妙な関係

C++11標準ライブラリの条件変数std::condition_variableおよびstd::condition_variable_anyは仕様上 spurious wakeup を許容する(→id:yohhoy:20120326)。このため条件変数のタイムアウト付き待機関数wait_until, wait_forの動作において、「通知を受けていないしタイムアウトもしていないがスレッドのブロックが解除される」という、プログラマの望まない(しかし規格に準拠した)状況が発生しうる。

#include <mutex>
#include <condition_variable>

int user_cond = 0;
std::mutex mtx;  // 変数user_condの保護
std::condition_variable cv;  // 待機条件の達成(user_cond!=0)を通知

/* 別スレッドにて user_condへの代入、cv通知 を実行 */

// 相対時間タイムアウトありで条件user_cond!=0のときtrueを返す関数...?
bool check_user_cond(int sec)
{
  const auto rel_time = std::chrono::seconds(sec);
  std::unique_lock<std::mutex> lk(mtx);
  if (user_cond == 0) {
    if (cv.wait_for(lk, rel_time) == std::cv_status::timeout)
      return false;
  }
  // BUG: spurious wakeupが起きた場合は誤った結果を返す
  return true;
}

誤った修正

spurious wakeup に対応するため、waitメンバ関数のときと同様に条件変数での待機処理と待機条件のチェックをループ中に記述しなければならない。ただし相対時間でタイムアウトを指定するwait_forメンバ関数の場合、単純なループ構造への置き換えでは別の問題が生じる。

// BUG: if→whileへの単純置換
bool check_user_cond(int sec)
{
  const auto rel_time = std::chrono::seconds(sec);
  std::unique_lock<std::mutex> lk(mtx);
  while (user_cond == 0) {  // ★ ifをwhileループに置き換え
    if (cv.wait_for(lk, rel_time) == std::cv_status::timeout)
      return false;
  }
  return true;
}

タイムアウト指定が10秒と仮定して説明する。wait_forでスレッドがブロックされてから9秒後に spurious wakeup によってブロック解除された場合、同関数はcv_status::no_timeoutを返すため、再びタイムアウト10秒での待機処理が行われる。spurious wakeup の発生はユーザコードからは制御できないため、「指定したタイムアウト時間10秒を大幅に超過したのにcheck_user_cond関数が制御を戻さない」といった不都合が生じる可能性がある。(極端な最悪ケースとして、永遠に制御が戻らない可能性もありえる。)

正しい修正

下記に2種類の修正コードを示す。元のタイムアウト付き待機関数(std::cv_status列挙型)とそのPredicate指定版(bool型)とで、メンバ関数の戻り値型が異なることに注意。可能な限りコーディング誤りのリスクが小さい修正Aが望ましい。

// 修正A:Predicate指定版wait_forを利用
bool check_user_cond(int sec)
{
  const auto rel_time = std::chrono::seconds(sec);
  std::unique_lock<std::mutex> lk(mtx);
  return cv.wait_for(lk, rel_time, [&]{ return user_cond != 0; });
}
// 修正B:絶対時刻とwait_until利用に変換
bool check_user_cond(int sec)
{
  // 相対時間→絶対時間に変換
  const auto abs_time = chrono::steady_clock::now() + std::chrono::seconds(sec);
  std::unique_lock<std::mutex> lk(mtx);
  while (user_cond == 0) {
    // wait_for→wait_untilに置換
    if (cv.wait_until(lk, abs_time) == std::cv_status::timeout)
      return false;  // (1) タイムアウト...?
  }
  return true;
}

修正Bでは(1)にて無条件にfalseを返しているが、「タイムアウトと条件変数への通知が同時に発生した」場合は、待機条件を満たしていても誤った結果を返してしまう。この誤判定を防ぐには、下記コードのように待機条件の再チェックが必要となる*1

// 修正B':絶対時刻とwait_until利用に変換
bool check_user_cond(int sec)
{
  const auto abs_time = chrono::steady_clock::now() + std::chrono::seconds(sec);
  std::unique_lock<std::mutex> lk(mtx);
  while (user_cond == 0) {
    if (cv.wait_until(lk, abs_time) == std::cv_status::timeout)
      return (user_cond != 0);  // 待機条件を再評価
  }
  return true;
}

関連URL

*1:修正B'は Predicate 指定版 wait_for メンバ関数の Effect に等しい。(N3337 30.5.1/p32, p39)