スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方

· 更新日: · · Windows, マルチスレッド, 条件変数, 同期, C++, C#, Win32 API, 不具合調査

更新履歴(3件・最終更新 2026年09月06日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

条件変数の技術的な主張とコード例を維持し、偽の起床・横取り・起こし損ねの違い、言語別の待機方法、症状別の調査手順を追いやすい構成に整理した。
知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
知識マップの2つの関係を改めました。述語のwhileループが防ぐ対象を「スプリアスウェイクアップ・横取り」から「条件不成立のまま処理が進むこと(競合状態)」に、カーネルAPCが引き起こすものを「PulseEvent」から「起こし損ね」に修正しています。目覚め自体は防げず、防げるのは誤った続行だという本文の説明に図を合わせたものです。本文の説明は変えていません。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.22054277)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方」合同会社小村ソフト. https://doi.org/10.5281/zenodo.22054277 https://comcomponent.com/blog/windows-condition-variable-spurious-wakeup/

DOI(最新版)
10.5281/zenodo.22054277
DOI(この版)
10.5281/zenodo.22450724

「データを入れてから通知しているのに、ワーカーが空のキューを読んで落ちる」「通知したはずなのに、ときどき待機から戻らない」。どちらも待ち合わせの不具合ですが、調べるべき箇所は違います。

本記事の中心は、条件変数の wait から戻ったことは、待っていた条件の成立を保証しないという原則です。通知なしに戻るスプリアスウェイクアップと、通知後に条件を消費される横取りを理解すると、なぜ if ではなく while が必要なのかが分かります。通知を取り逃す起こし損ねも、これらとは分けて扱います。

知りたいこと・困っていること 読む箇所
通知なしに目覚める理由と、横取りとの違いを知りたい 戻った時点で何が保証されるか、仕様が許す理由
Win32・C++・C#での正しいコードを確認したい 言語別の基本形
タイムアウトを指定したのに、待ち時間が延びる 締め切りを基準にした待機
通知したのにスレッドが戻らない 起こし損ねとPulseEvent
まれに起きるクラッシュ・ハングを調査したい 症状別の調査手順

対象は、Windowsで業務アプリや装置制御ソフトを書く開発者です。一次情報で仕組みを確認し、Win32(C)・C++・C#の実装へつなげます。

1. まず結論

待機コードで守ることは、次の三つです。

原則 コードで守ること
通知ではなく、状態を待つ 「起こされたか」ではなく「キューが空でないか」などを条件にする
戻るたびに、条件を確認する while (!条件) wait(...)、C++なら述語付きの wait(lock, pred) を使う
確認と更新を、同じロックで保護する 状態を更新してから通知し、確認と待機の隙間で通知を取り逃さないようにする

Win32・C++・POSIXは、明示的な通知と結びつかない目覚めを仕様として許しています。加えて、通知があっても別のスレッドが先に条件を消費する「横取り」があるため、再確認は必須です。C#の Monitor.Wait も横取りを考慮した同じ規律で使います。12345

もう一つの注意は、条件変数の一時的な通知をイベントのパルスで再現しないことです。特に PulseEvent は通知を取り逃す場合があり、Microsoftは新しいアプリで使わず、代わりに条件変数を使うよう案内しています。6

以下は、目覚めの仕組み(2〜4章)、正しい実装(5章)、避けるパターンと調査(6〜7章)、点検表(8章)の順です。

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全17件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. スプリアスウェイクアップとは何か ── 「目覚めた=条件成立」ではない

2.1 条件変数が行うのは、ロックを放して待ち、取り直して戻ること

条件変数(condition variable)は、ある条件が成立するまでスレッドを待たせる同期プリミティブです。Win32では、次の構造体とAPIを使います。1

役割 Win32の型・API
条件変数を表す CONDITION_VARIABLE
ロックを解放して待つ SleepConditionVariableCS / SleepConditionVariableSRW
待機中のスレッドを起こす WakeConditionVariable / WakeAllConditionVariable

待機APIは、保持しているクリティカルセクションまたはSRWロックの解放と待機への移行を不可分に行います。目覚めた後は、そのロックを再取得してから呼び出し元へ戻ります。1

ただし、ロックを取り直して戻ったことと、アプリが待っていた条件の成立は別です。

2.2 正規の通知、スプリアスウェイクアップ、横取りを分ける

タイムアウトの扱いは5章で確認するとして、まず目覚めの三つのケースを比べます。

ケース 明示的な通知との関係 戻った時点での読み方
正規の目覚め 通知がある 条件が成立していることは多いが、戻ったという事実だけでは保証されない
スプリアスウェイクアップ(偽の起床) 自分を起こす明示的な通知と結びつかない 条件が不成立のままでも戻る
横取り(stolen wakeup) 通知はある 別のスレッドが先に条件を消費し、不成立に戻っている
waitから戻る3つのケース条件変数のwaitからは、正規の通知による目覚めのほかに、通知なしのスプリアスウェイクアップと、通知はあったが条件を先に消費された横取りでも戻るため、どのケースでも条件の再確認が必要になるwaitから戻った正規の通知スプリアスウェイクアップ(通知なし)横取り(条件は消費済み)条件を再確認してから進む

図1: waitから戻る経路は3つあり、どれで戻ったかを呼び出し側は区別できないため、必ず条件を再確認する。

スプリアスウェイクアップは「システム全体で通知を一度も送っていない場合」に限りません。短時間に通知が集中すると、実装の都合で待機スレッドが余分にまとめて起こされることがあります。対応する明示的な通知のないスレッドから見れば、これも偽の起床です。

Microsoft Learnは、条件変数にはスプリアスウェイクアップと横取りの両方があるため、待機から戻ったら述語を、通常は while で再確認するよう求めています。述語とは、ここでは「キューが空でない」などの待っている条件です。1

2.3 目覚めた理由ではなく、現在の条件で進むかを決める

呼び出し側は、この三つのどれで戻ったかを区別できません。そこで、戻るたびに条件そのものを確認し、不成立ならもう一度待つという形にします。

while は偽の起床や横取りそのものを防ぐのではありません。どちらが起きても、条件が不成立のまま処理を続けないためのものです。この再確認と、5章で扱う同じロックによる保護を守れば、目覚めの経路によらず正しく処理できます。

3. なぜ仕様として許されているのか ── 正確な通知は高くつく

3.1 厳密な通知のコストを、すべての操作へ負担させないため

スプリアスウェイクアップを起こさない実装は、理論上は可能です。それでもPOSIX・Windows・C++が許容する理由は、性能と、待機側で条件を再確認する設計にあります。POSIXの pthread_cond_wait のRationale(根拠)も、この判断を説明しています。3

「ちょうど1つだけを確実に起こす」通知を厳密に実装すると、特にマルチプロセッサでは、条件変数の各操作に余分な同期コストがかかります。通知と目覚めの間にはスケジューラーが介在し、割り込みやプリエンプションもあります。まれな余分の起床を許し、待機側で再確認するほうが、実装を速く保てます。3

再確認ループには、意図をコードに示して堅牢にする利点もあります。通知を「条件成立の保証」ではなく、条件が変わったかもしれないというヒントとして扱えば、余分に起こす、まとめて起こす、といった通知側の変更にも耐えられます。3

3.2 偽の起床をなくしても、横取りは残る

横取りは、通知からロックの再取得までの時間差で起きます。生産者がキューへ1件入れて消費者Aを起こしても、Aがロックを取り直す前に消費者Bがその1件を取れば、Aが戻る頃にはキューは空です。

横取り(stolen wakeup)が起きる時系列生産者がキューに1件入れて待機中の消費者Aを起こすが、Aがロックを再取得する前に消費者Bがロックを取得して1件取り出してしまい、Aが目覚めたときにはキューが空になっている消費者B生産者消費者A(待機中)消費者B生産者消費者A(待機中)目覚めたがロック再取得待ちキューは空(横取りされた)キューに1件追加WakeConditionVariableロックを取得して1件取り出すロックを再取得してwaitから復帰whileで再確認し再びwait

図2: 通知から目覚めまでの時間差の間に、第三のスレッドが条件を消費する「横取り」はどんな実装でも起こりうる。

ロックを先に取れる第三のスレッドがいる限り、この横取りは実装を磨くだけでは消せません。偽の起床を完全になくせたとしても、横取りがある以上、待機側のループは必要です。それなら偽の起床を許して条件変数を速く保つ、という設計上の判断になります。

4. Windowsではどの層で現れるのか

4.1 共通するのは再確認の規律で、目覚めの理由は区別する

API・ライブラリ 再確認が必要な理由
Win32の条件変数 スプリアスウェイクアップと横取りが明記されている2
WaitOnAddress 指定アドレスへのシグナル以外の理由でも、早期に戻ることが許されている7
C++の std::condition_variable 述語なしの wait はスプリアスに目覚めることがある48
.NETの Monitor.Wait 起こされてからロックを取り直すまでの間に、別スレッドが条件を消費できる5

Win32の公式の生産者・消費者キューのサンプルも、待機を while ループで書いています。9

WaitOnAddress はWindows 8以降の、指定アドレスの値が変わるのを待つ低レベルAPIです。公式資料は、シグナル以外で早期に戻る例として、低メモリ状態、同じアドレスに対する以前のwakeの放棄、checkedビルドでの実行を挙げています。使用例も値を比較し直す while ループです。7

4.2 C++の述語付きwaitは、ループを代行する

MSVCの資料は、wait(lock, pred) が実質的に次を実行すると説明しています。4

while (!Pred())
    wait(Lck);

述語なしの wait がスプリアスに戻り得ることはcppreferenceにも明記されています。述語付きの形なら、この再確認ループをライブラリに任せられます。8

4.3 C#では、横取りとロック再取得に注目する

Monitor.Wait / Pulse は、待機キューとレディキューを使います。Pulse / PulseAll で起こされたスレッドはレディキューへ移り、ロックを取り直してから Wait を抜けます。その間に、別スレッドが先に条件を消費できる点はWin32と同じです。510

.NETの Monitor.Wait では、条件変数のような理由のない目覚めを想定するのではなく、横取りやタイムアウトがあるため、同じ while の規律が必要になると分けて考えます。ドキュメントも、待機に入った原因の条件を評価し直し、必要なら再び Wait する使い方を想定しています。5

どの層でも述語の再確認が要求されるC++のstd::condition_variable、.NETのMonitor、Win32のCONDITION_VARIABLE、低レベルのWaitOnAddressのいずれの層でも、公式ドキュメントが目覚めた後の条件再確認を要求しているC++ std::condition_variable目覚めたら条件を再確認(while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

図3: 言語やフレームワークを変えても、待機プリミティブの層ではどこでも「目覚めた後の再確認」が公式に要求されている。

5. 正しい待ち方 ── whileと述語で書く

共通の形: 条件を確認し、成立したときだけ処理する

待つ対象は「通知を受けたか」ではなく、ロックで保護された共有状態です。キューの件数やフラグを同じロックの中で確認・更新し、不成立なら wait、戻ったら再確認します。

正しい待機ループの流れロックを取得して条件を確認し、不成立ならロックを放して眠り、目覚めたらロックを再取得して再び条件確認に戻る。成立していたときだけロックを持ったまま処理に進むいいえはいロックを取得条件は成立?wait(ロックを放して眠る)目覚める(ロックを再取得)ロックを持ったまま処理する

図4: 正しい待機はループであり、条件確認と処理の間に隙間がない(どちらもロック保持中に行われる)。

この形なら、ループを抜けた時点で、ロックを持ったまま条件が成立していることを確認できています。そのまま状態を消費するので、条件確認と処理の間に、他スレッドが割り込む隙間を作りません。

Win32(C)での基本形

CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0;   // csで保護される共有状態

// 起動時に一度だけ初期化する(静的初期化なら cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);

// 待機側(消費者)
EnterCriticalSection(&cs);
while (queueCount == 0) {                       // if ではなく必ず while
    SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// ここではロックを保持し、queueCount > 0 が保証されている
--queueCount;
LeaveCriticalSection(&cs);

// 通知側(生産者)
EnterCriticalSection(&cs);
++queueCount;                                    // 状態の更新はロックの中で
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv);                      // 通知はロック解放後でよい

区別したいのは、状態の更新と、通知を呼ぶ場所です。++queueCount は必ずロック内で行います。一方、WakeConditionVariable はロック内・外のどちらからでも呼べます。Microsoftは、コンテキストスイッチを減らすため、通常はロックを解放してから起こすほうがよいとしています。1

C++での基本形 ── 述語付きwaitを既定に

std::mutex m;
std::condition_variable cv;
std::queue<Item> q;

// 待機側
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); });   // 内部で while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();

// 通知側
{
    std::lock_guard<std::mutex> lk(m);
    q.push(std::move(item));
}
cv.notify_one();

新規コードは述語付き wait を既定にします。既存の while (q.empty()) cv.wait(lk); も正しい形なので、手書きのループだからといって直す必要はありません。問題なのは、再確認しない if (q.empty()) cv.wait(lk); です。

述語付きの形が代行するのはループだけです。述語が読む共有状態を、通知側でも同じミューテックスで保護し、状態を更新してから通知する規律は引き続き必要です。

C#での基本形

private readonly object _gate = new();
private readonly Queue<Item> _queue = new();

// 待機側
lock (_gate)
{
    while (_queue.Count == 0)          // if ではなく必ず while
    {
        Monitor.Wait(_gate);
    }
    var item = _queue.Dequeue();
}

// 通知側
lock (_gate)
{
    _queue.Enqueue(item);
    Monitor.Pulse(_gate);              // MonitorのPulseはロック内でのみ呼べる
}

Monitor.Wait / Pulse / PulseAll は、対象のロックを保持した同期ブロック内で呼びます。ロック外では SynchronizationLockException になります。Win32の通知をロック外へ出せることと混同しないでください。10

タイムアウト付きの待機 ── 残り時間は締め切りから計算する

毎回同じタイムアウト値を渡すと、偽の起床で戻るたびに待ち時間を積み増してしまいます。先に締め切りを決め、戻るたびに残り時間を計算する形にします。

ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
    ULONGLONG now = GetTickCount64();
    if (now >= deadline) {
        break;                          // タイムアウト(条件は不成立のまま)
    }
    if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
        GetLastError() != ERROR_TIMEOUT) {
        break;                          // タイムアウト以外の失敗は待機をやめて抜ける
    }
    // ERROR_TIMEOUTはwhileの条件判定と締め切りチェックで最終確認する
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
タイムアウト付き待機の正しい流れ先に締め切り時刻を決め、目覚めるたびに条件と締め切りを確認し、まだであれば残り時間を計算し直して待機に戻るはいいいえはいいいえ締め切り時刻を決める条件は成立?処理へ進む締め切りを過ぎた?タイムアウト処理残り時間を計算してwait

図5: タイムアウト付き待機は「同じ待ち時間」を渡し直すのではなく、締め切り時刻を基準に残り時間を計算し直す。

C++では、絶対時刻を指定する wait_until と述語のオーバーロードに、この処理を任せられます。タイムアウトの場合も述語の最終値を返すため、最終的に条件が成立したかで判定できます。4

6. やってはいけないパターン集

6.1 ifで一度だけ条件を確認する

偽の起床や横取りが起きると、条件が不成立のまま続行してしまいます。空キューからの取り出し、未初期化データの参照、二重解放など、まれなクラッシュやデータ破損として現れます。5章の while または述語付き wait にします。

6.2 条件の確認・更新をロックの外で行う

こちらは「余分に目覚める」のではなく、通知を取り逃して眠り続ける、起こし損ね(lost wakeup)の問題です。

待機側がロック外で条件を見て「まだだ」と判断してから待機へ入るまでに、通知側が状態を更新して通知すると、その瞬間には待機者がいません。通知が消えた後に待機へ入り、次の通知がなければ待ち続けます。

起こし損ね(lost wakeup)が起きる時系列待機側がロックの外で条件を確認してからwaitに入るまでの隙間に、通知側が状態を更新して通知すると、通知は待機者のいない条件変数に送られて消え、待機側は二度と来ない通知を待ち続ける通知側待機側通知側待機側このとき待機者はいない通知は既に消えており目覚めないロックの外で条件を確認(不成立)状態を更新して通知waitに入る

図6: ロックの外で条件を確認すると、確認とwaitの隙間に通知が素通りする「起こし損ね」が起きる。

条件変数の待機APIが、ロックの解放と待機への移行を不可分にするのは、この隙間を閉じるためです。条件の確認・更新を同じロックで保護し、ロックを保持して待機APIへ入る規律を守れば、この起こし損ねを防げます。1

6.3 一時的な通知を、イベントのパルスで再現する

CreateEvent と SetEvent を使うこと自体はアンチパターンではありません。イベントには、次のような正しい使いどころがあります。

用途 イベントを使う場面
一つの消費者を起こす 起こされた消費者が、キューを空になるまで処理する構成
停止を知らせる 一度立てたら下ろさない停止指示を、手動リセットイベントで表す
他の待機対象とまとめて待つ WaitForMultipleObjects に組み込む
プロセスをまたいで待ち合わせる プロセス間で共有できない条件変数では扱えない用途

条件変数は、プロセス間で共有できないユーザーモードオブジェクトです。用途によっては、条件変数へ置き換えること自体ができません。1

危ないのは、条件変数の「その瞬間の待機者だけを起こし、通知の状態を残さない」動作をイベントで再現しようとすることです。その発想が、次の PulseEvent の問題につながります。

6.4 PulseEventを使う

PulseEvent は、手動リセットイベントでは、その瞬間に待っているスレッドを起こして、すぐ非シグナルへ戻すAPIです。しかしMicrosoftは、信頼できず主に後方互換のために存在するため、新しいアプリでは使わず、代わりに条件変数を使うよう明記しています。6

理由は、待機中のスレッドがカーネルモードAPCで一時的に待機状態から外れ、APC完了後に待機へ戻ることがあるためです。その間に PulseEvent が呼ばれても、そのスレッドは「呼ばれた瞬間の待機者」に含まれず、起こされません。カーネルAPCはOS内部の動作であり、アプリ側では制御できません。611

この問題は静的解析の警告C28648にもなっています。12 偽の起床は「余分に目覚める」問題ですが、こちらは「目覚めるべきなのに目覚めない」問題です。通知自体が失われるため、待機を while で囲むだけでは救えません。

6.5 状態を更新する前に、ロックを持たずに通知する

ロックを持たずに先に通知し、その後でロックを取って状態を更新すると、起こされた側が古い状態を見て再び眠る可能性があります。更新後の通知がなければ、そのままです。

ただし、次の二つは区別します。

順序 結果
ロック外で通知し、後からロックを取って状態を更新する 起こされた側が更新前に再確認して眠る隙間がある
同じロックを保持したまま、通知→更新→解放と進む 待機側はロック再取得まで確認できないため、この順序による実害はない

後者を毎回安全性の検討対象にするより、同じロックの中で状態を更新し、その後に通知する順序へ統一するのが分かりやすく安全です。

7. 遭遇したときの調べ方

7.1 「条件不成立でも進む」か「目覚めない」かを分ける

症状 最初に調べること 調査方法
空キューからの取り出し、クラッシュ、処理結果の欠落 待機後に条件を再確認しているか 述語なしの cv.wait(、SleepConditionVariableCS、Monitor.Wait の周囲を検索する
起きるはずのスレッドが戻らずハングする 起こし損ねや PulseEvent がないか ダンプで各スレッドのスタックを確認し、止まっている待機APIから通知側を追う

述語なしの cv.wait(lk) を見つけても、正しい while に囲まれていれば問題ありません。検索で候補を集め、その周囲が if だけになっていないか、条件の確認・更新を同じロックで行っているかをレビューします。再現を待たずに点検できる箇所です。

ハングでは、ダンプで待機箇所を特定した後、「誰が、どの順序で通知するはずだったか」をコードで追います。ロック外の条件確認、状態更新前のロック外通知、PulseEvent を確認します。

症状からの切り分けフロー条件不成立のまま処理が進む症状なら述語なし待機をコード検索で洗い出し、スレッドが目覚めない症状ならダンプで待機箇所を特定して起こし損ねとPulseEventを疑うまれにしか出ない不具合条件不成立のまま処理が進む起きるべきスレッドが起きない述語なし待機をコード検索ダンプで待機中のスレッドを特定ifをwhile・述語付きwaitに直す起こし損ねとPulseEventを疑う

図7: 症状が「進みすぎ」か「目覚めない」かで、疑う箇所と調査手段が分かれる。

7.2 修正前後を、同じストレス条件で比べる

まれな不具合を再現するには、競合の窓を広げ、タイミングのばらつきを増やします。スレッド数を物理コア数より多くする、待機と通知の間に調査用の Sleep を入れる、デバッグ・リリースの両ビルドで試す、といった方法があります。

「待機を直したら再現しなくなった」と比較するときも、修正前後で同じストレスをかけます。

8. まとめ ── チェックリスト

通知は条件が変わったかもしれないというヒントであり、処理を続ける根拠は、ロック内で確認した条件そのものです。

点検項目 正しい形
待機から戻った後 正規の通知・偽の起床・横取りを区別しようとせず、条件を再確認する
待機の書き方 while で囲む。C++の新規コードは述語付き wait(lock, pred) を既定にする
共有状態 確認と更新を同じロックで保護し、状態を更新してから通知する
通知を呼ぶ場所 Win32/C++はロック解放後でもよい。C#の Pulse はロック内で呼ぶ
タイムアウト 締め切りから残り時間を計算する。C++なら述語付き wait_until を使う
イベントの使い方 正しい用途と一時的なパルスを区別し、PulseEvent に依存しない

スプリアスウェイクアップはWin32・C++・POSIXが意図して許した仕様です。OSの修正を待つことやライブラリの乗り換えではなく、待機側の規律で対処します。.NETの Monitor.Wait は理由のない目覚めとは区別しつつ、横取りやタイムアウトに備えて同じ再確認を行います。

クラッシュなら「条件が不成立でも進む箇所」、ハングなら「通知を取り逃す箇所」から調べる。この区別とチェックリストを、既存コードのレビューにも使ってください。

関連記事

関連する相談領域

合同会社小村ソフトでは、マルチスレッドコードの設計レビュー、「たまにしか再現しない」クラッシュ・ハングの原因調査(ダンプ解析)、レガシーな同期コード(イベント・PulseEvent依存など)の条件変数ベースへの改修を扱っています。現象の切り分けからで構いませんので、お気軽にご相談ください。

参考リンク

  1. Microsoft Learn, Condition Variables. 条件変数がロックの解放と待機への移行を不可分に行うユーザーモードオブジェクトであること、スプリアスウェイクアップ(明示的な起床と結びつかない目覚め)と横取り(起こされたスレッドより先に別のスレッドが実行される)があるため待機から戻ったら述語をwhileループで再確認すべきこと、通知はロックの中からでも外からでも行えるがコンテキストスイッチを減らすにはロック解放後に起こすほうがよいことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). 指定したクリティカルセクションの解放と条件変数での待機を不可分に行うこと、目覚めたスレッドはクリティカルセクションを再取得してから戻ること、タイムアウト時はERROR_TIMEOUTが返ること、スプリアスウェイクアップと横取りがあるため待機から戻ったら述語を(通常はwhileループで)再確認すべきことについて。 ↩ ↩2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. pthread_cond_wait / pthread_cond_timedwaitからのスプリアスウェイクアップが起こりうること、waitから戻ったことは述語の値について何も意味しないため述語を再評価すべきこと、Rationaleで「ちょうど1つだけ起こす」実装が特にマルチプロセッサで条件変数操作を遅くしうること、スプリアスウェイクアップの許容が述語確認ループを強制しアプリケーションを堅牢にすることが述べられている点について。 ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, condition_variable Class. 述語なしのwaitがnotify_one / notify_allで解除されるほかスプリアスに目覚めることもあると明記されていること、述語付きのwait(lock, pred)が実質的に while (!Pred()) wait(Lck); を実行すること、wait_for / wait_untilにも同じ性質と述語付きオーバーロードがあることについて。 ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn, Monitor.Wait Method. Waitがロックを解放して待機キューに入ること、Pulse / PulseAllで起こされた後もロックを再取得するまで戻らないこと、起こされたスレッドは待機に入った原因となった条件を評価し直し必要ならもう一度Waitを呼ぶという使い方が想定されていることについて。 ↩ ↩2 ↩3 ↩4

  6. Microsoft Learn, PulseEvent function (winbase.h). 待機中のスレッドがカーネルモードAPCで一時的に待機状態から外されAPC完了後に戻ることがあり、その間にPulseEventが呼ばれるとそのスレッドは解放されないこと、このためPulseEventは信頼できず新しいアプリケーションで使うべきでなく代わりに条件変数を使うべきであることについて。 ↩ ↩2 ↩3

  7. Microsoft Learn, WaitOnAddress function (synchapi.h). アドレスの値が変わるのを待つ関数がシグナル時に戻ることは保証されるが他の理由で戻ることも許されていること、早期に目覚める例として低メモリ状態・同じアドレスへの以前のwakeの放棄・checkedビルドでの実行が挙げられていること、そのため戻ったら値を比較し直すべきで公式サンプル自体がwhileループであることについて。 ↩ ↩2

  8. cppreference.com, std::condition_variable::wait. 述語なしのwaitがスプリアスウェイクアップで解除されうること、述語付きオーバーロードが while (!pred()) wait(lock); と等価であり、通知やスプリアスウェイクアップのたびにロックを再取得して述語を確認するループとして定義されていることについて。 ↩ ↩2

  9. Microsoft Learn, Using Condition Variables. 1つのクリティカルセクションと2つの条件変数(BufferNotEmpty・BufferNotFull)で生産者・消費者キューを実装する公式サンプルについて。待機は述語を確認するループの中で行われている。 ↩

  10. Microsoft Learn, Monitor.PulseAll Method. PulseAllが待機キューのスレッドをレディキューに移し、ロックが解放されるとレディキューの次のスレッドがロックを取得すること、Pulse / PulseAll / Waitは同期ブロックの中からしか呼べないことについて。 ↩ ↩2

  11. Microsoft Learn, Waits and APCs. カーネルAPCがプリエンプティブに実行され、待機APIを戻すことなくシステムが内部的に待機を中断・再開すること、その間にKePulseEventのような一過性のシグナルを見逃しうることについて。 ↩

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. 静的解析がPulseEventの使用に警告を出すこと、APCで待機から外れていたスレッドは解放されず永久にハングしうること、SetEventや別の同期オブジェクトへの置き換えの指針について。 ↩

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

スプリアスウェイクアップはOSやライブラリのバグですか?
バグではなく、仕様として明文化された挙動です。Win32のSleepConditionVariableCS、C++のstd::condition_variable、POSIXのpthread_cond_waitのいずれも、公式ドキュメントや規格が「通知と結びつかない目覚めが起こりうる」と明記しています。これを禁止する実装も理論上は可能ですが、条件変数の全操作(特にマルチプロセッサでの通知)が遅くなるため、「待機側が条件を再確認すれば正しさは保てる」という割り切りで許容されています。したがって対処はOS側の修正を待つことではなく、waitを必ずwhileループ(または述語付きwait)で書くことです。
waitをwhileで囲むと性能が落ちませんか?
実用上は無視できます。whileで囲んだ場合に増えるのは、目覚めるたびに行う条件のチェック1回分だけで、これはロックを保持した状態での軽い比較です。スプリアスウェイクアップ自体もまれにしか起きないため、余分なループが回るのは例外的な場面に限られます。一方でifのまま放置した場合の代償は、条件不成立のまま処理が進む「まれにしか再現しないバグ」であり、比較になりません。条件変数の待機コストで本当に効くのはロックの競合と通知の頻度で、whileの有無ではありません。
C++の述語付きwaitを使えばスプリアスウェイクアップを意識しなくてよいですか?
待機ループについてはその通りで、cv.wait(lock, pred)は実質的にwhile (!pred()) wait(lock);を実行するため、スプリアスウェイクアップも横取りも自動的に吸収されます。新規のC++コードでは述語付きオーバーロードを既定にすべきです。ただし、述語が参照する共有状態の更新を同じミューテックスで保護すること、通知側が状態を更新してからnotifyを呼ぶことは、依然として自分で守る必要があります。述語付きwaitはループを肩代わりするだけで、ロックの規律まで肩代わりしてくれるわけではありません。
C#のMonitor.Waitでも同じ問題は起きますか?
起きます。Monitor.Waitで待っていたスレッドはPulse/PulseAllで起こされた後、ロックを取り直してからWaitを抜けますが、その間に別のスレッドが先にロックを取得して条件を消費しているかもしれません(横取り)。Microsoftのドキュメントも、起こされたスレッドは待機に入った原因の条件を評価し直し、必要ならもう一度Waitを呼ぶ、という使い方を想定して書かれています。したがってC#でもwhile (!条件) Monitor.Wait(gate);という形が基本です。lockステートメントの中でしかWait/Pulseを呼べない点はWin32と異なる制約です。
WaitForSingleObjectでイベントを待つ場合にもスプリアスウェイクアップはありますか?
通常の(alertableでない)待機では、WAIT_OBJECT_0が返るのはオブジェクトが実際にシグナル状態になったときで、条件変数のような「理由のない目覚め」は起きません。ただし「イベントがシグナルになった」ことと「自分のアプリの条件が成立している」ことは別です。複数の消費者が同じイベントで起こされる設計なら、先にロックを取ったスレッドが条件を消費するため、結局は目覚めた後の条件確認が必要になります。また、条件変数のような「その瞬間の待機者だけを起こす」一時的な通知をイベントで再現しようとする設計はPulseEventの信頼性問題に行き着きやすいため、プロセス内で状態の成立を待つ用途には条件変数を使うのが安全です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る