Spurious wakeup — mengapa condition variable "bangun tanpa notifikasi" dan cara menunggu yang benar di Windows

· Diperbarui pada: · · Windows, Multithreading, Condition variable, Sinkronisasi, C++, C#, Win32 API, Investigasi bug

Riwayat revisi (3 pembaruan, terakhir pada 31 Aug 2026)

Catatan perubahan yang dilakukan pada artikel ini. Jika versi sebelumnya telah diarsipkan, versi itu tetap dapat dibaca melalui tautan permanen dengan DOI.

Diterjemahkan ulang sebagai terjemahan lengkap dari naskah Jepang. Versi bahasa Indonesia sebelumnya adalah ringkasan yang hanya memindahkan sebagian naskah, sehingga bagian, tabel, gambar Mermaid, keterangan gambar, dan FAQ tidak ada. Semuanya dipulihkan sesuai naskah Jepang, dan klaim teknisnya sama dengan versi Jepang.
Memeriksa ulang semua relasi peta pengetahuan dan memperbaiki relasi yang tidak selaras dengan teks penjelasan (arah terbalik, generalisasi berlebih, dan tertukarnya predikat). Penjelasan di badan artikel tidak diubah.
Mengubah dua relasi pada peta pengetahuan. Sasaran yang dicegah loop while predikat diubah dari "spurious wakeup / stolen wakeup" menjadi "pemrosesan lanjut padahal kondisi belum terpenuhi (race condition)", dan yang ditimbulkan kernel APC diubah dari "PulseEvent" menjadi "lost wakeup". Ini menyesuaikan gambar dengan penjelasan badan artikel bahwa bangun itu sendiri tidak dapat dicegah; yang dapat dicegah adalah kelanjutan yang salah. Penjelasan di badan artikel tidak diubah.
Publikasi pertama
Mengutip artikel ini(DOI (arsip terdaftar): 10.5281/zenodo.22176639)

DOI di bawah mengarah ke versi yang telah diarsipkan sebelumnya dan mungkin berbeda dari teks saat ini. Gunakan URL halaman ini untuk merujuk teks saat ini.

Go Komura (2026). Spurious wakeup — mengapa condition variable "bangun tanpa notifikasi" dan cara menunggu yang benar di Windows. KomuraSoft LLC. https://comcomponent.com/id/blog/windows-condition-variable-spurious-wakeup/

DOI (arsip terdaftar)
10.5281/zenodo.22176639
DOI (versi terakhir yang didaftarkan)
10.5281/zenodo.22176640

“Data dimasukkan ke antrian, lalu thread worker yang menunggu dibangunkan. Sudah berjalan enam bulan, lalu suatu hari mencoba membaca antrian kosong dan crash.” “Notifikasi mestinya sudah dikirim, tetapi kadang ada thread yang tidak bangun.” — Penantian antar-thread tampak seolah berjalan, padahal justru sarang bug yang hanya muncul jarang. Investigasi semacam ini sering berujung pada kode yang membungkus wait condition variable dengan if. Dan yang ada di belakangnya adalah spurious wakeup (bangun palsu) — fenomena keluar dari wait meskipun notifikasi tidak diterima.

Mendengar “bangun padahal tidak ada yang memberitahu” terasa seperti cacat implementasi, tetapi itu spesifikasi yang Win32, C++, dan POSIX cantumkan bersama di dokumentasi atau standar, dan Monitor di .NET pun dirancang dengan asumsi “setelah dibangunkan, periksa ulang kondisi”. Mengapa perilaku itu diizinkan? Di lapisan mana ia muncul di Windows? Dan bagaimana menulisnya agar tidak pernah terkena masalah itu? Artikel ini ditujukan kepada pengembang yang menulis aplikasi bisnis atau perangkat lunak kontrol peralatan di Windows: mengurai identitas spurious wakeup berdasarkan sumber primer, lalu menurunkannya menjadi cara menunggu yang benar di Win32 (C), C++, dan C#.

1. Kesimpulan dulu

  • wait pada condition variable dapat kembali meskipun notifikasi tidak datang. Dokumentasi resmi Win32 menyatakan bahwa condition variable tunduk pada spurious wakeup (bangun yang tidak terikat pada bangun eksplisit) dan stolen wakeup (thread lain mengonsumsi kondisi sebelum thread yang dibangunkan).1
  • Karena itu, tunggu selalu ditulis sebagai “loop while + pemeriksaan ulang kondisi”. Kode yang memeriksa sekali dengan if lalu wait tampak seolah berjalan, padahal menyimpan bug yang hampir tidak pernah bisa direproduksi.12
  • Ini bukan keanehan khusus Windows; POSIX dan standar C++ sama. Implementasi yang “sama sekali tidak pernah bangun palsu” akan memperlambat semua operasi condition variable, sehingga diizinkan dengan asumsi sisi yang menunggu akan memeriksa ulang.34
  • Di C++, wait(lock, pred) dengan predikat membuat pustaka menggantikan loop. Bentuk itu secara efektif menjalankan while (!pred()) wait(lock);. Itu bentuk bawaan untuk kode baru.5
  • Monitor.Wait di C# membutuhkan disiplin yang sama. Kondisi dapat dikonsumsi di sela antara dibangunkan dan memperoleh kembali kunci, jadi periksa kondisi dengan while lalu kembali ke Wait.6
  • Pembaruan dan pemeriksaan kondisi dilakukan di dalam kunci yang sama. Jika kondisi dilihat di luar kunci lalu masuk wait, notifikasi dapat lewat di celah itu — lost wakeup (gagal membangunkan).1
  • Jangan meniru notifikasi transien condition variable “bangunkan yang sedang menunggu sekarang” dengan pulse pada event. Khususnya PulseEvent: notifikasi dapat lewat saat APC mode kernel melepas tunggu sejenak, dan Microsoft sendiri menyatakan dengan tegas “tidak andal, jangan dipakai, pakai condition variable sebagai gantinya”.7

Berikutnya, mekanisme yang menopang kesimpulan ini ditelusuri berurutan.

Pada diagram, garis utuh menunjukkan relasi yang selalu berlaku dan garis putus-putus menunjukkan relasi bersyarat (syaratnya ada pada penjelasan masing-masing relasi di halaman rincian). Daftar lengkap relasi (total 17, beserta bukti dan tingkat kepastian) serta definisi konsep utama dikumpulkan di halaman rincian peta pengetahuan (dalam bahasa Jepang). Data: JSON-LD / Turtle

2. Apa itu spurious wakeup — “bangun” bukan berarti “kondisi terpenuhi”

Condition variable adalah primitif sinkronisasi untuk “menidurkan thread sampai suatu kondisi terpenuhi, lalu dibangunkan ketika itu terjadi”. Di Win32, itu adalah struktur CONDITION_VARIABLE beserta SleepConditionVariableCS / SleepConditionVariableSRW (tunggu) dan WakeConditionVariable / WakeAllConditionVariable (notifikasi). API tunggu melepas kunci yang dipegang (critical section atau SRW lock) dan beralih ke tidur secara atomik, lalu saat bangun memperoleh kembali kunci sebelum kembali.1

Masalahnya: fakta “kembali dari wait” sebenarnya berarti apa. Secara naif ingin berpikir “notifikasi datang = kondisi terpenuhi”, tetapi dalam kenyataan ada tiga kasus di mana wait kembali.

Kasus Notifikasi Kondisi pada saat kembali
Bangun yang sah Ada Sering terpenuhi, tetapi tidak dijamin
Spurious wakeup Tidak ada yang ditujukan ke thread ini Tetap tidak terpenuhi
Stolen wakeup (diserobot) Ada Thread lain sudah mengonsumsi lebih dulu; tidak terpenuhi
Tiga kasus kembali dari waitwait pada condition variable dapat kembali bukan hanya karena notifikasi yang sah, tetapi juga spurious wakeup tanpa notifikasi dan stolen wakeup di mana notifikasi ada tetapi kondisi sudah dikonsumsi lebih dulu, sehingga setiap kasus membutuhkan pemeriksaan ulang kondisiKembali dari waitNotifikasi yang sahSpurious wakeup (tanpa notifikasi)Stolen wakeup (kondisi sudah dikonsumsi)Periksa ulang kondisi, baru lanjut

Gambar 1: Ada tiga jalur kembali dari wait, dan pemanggil tidak dapat membedakan jalur mana yang diambil, jadi kondisi harus selalu diperiksa ulang.

Spurious wakeup adalah kasus kedua ini — fenomena API tunggu kembali tanpa terikat pada notifikasi eksplisit yang dimaksudkan untuk membangunkan thread itu. Ia tidak terbatas pada situasi di mana WakeConditionVariable tidak pernah dipanggil di seluruh sistem. Misalnya, di beban tinggi ketika notifikasi datang beruntun dalam waktu singkat, implementasi dapat membangunkan thread yang menunggu secara berlebih sekaligus; dari sisi yang tidak punya notifikasi yang sesuai, itu juga spurious wakeup. Halaman condition variable di Microsoft Learn menuliskannya dengan tegas. “Condition variables are subject to spurious wakeups (those not associated with an explicit wake) and stolen wakeups (another thread manages to run before the woken thread). Therefore, you should recheck a predicate (typically in a while loop) after a wait operation returns.”1

Yang penting: pemanggil tidak dapat membedakan dari ketiga kasus itu, mana yang membuatnya kembali. Karena tidak dapat dibedakan, hanya ada satu strategi yang tersedia. Setiap kali kembali, periksa kondisi yang ditunggu itu sendiri, dan jika belum terpenuhi, tidur lagi. Itulah isi sesungguhnya kaidah “bungkus wait dengan while”. Sebaliknya, selama kaidah itu dijaga, kode tetap benar tidak peduli dari ketiga kasus mana ia bangun.

3. Mengapa spesifikasi mengizinkannya — notifikasi yang tepat mahal

Pertanyaan “bangun padahal tidak diberitahu, bukankah itu kecerobohan implementasi?” wajar. Memang, secara teoretis mungkin membuat implementasi yang tidak pernah spurious wakeup. Meskipun begitu, POSIX, Windows, maupun standar C++ semuanya memilih sisi “dapat terjadi”. Alasannya ditulis terang-terangan di Rationale (dasar pemikiran) untuk pthread_cond_wait di POSIX (The Open Group Base Specifications).3

Alasan pertama adalah performa. Mencoba mengimplementasikan secara ketat notifikasi yang “pasti membangunkan persis satu thread” menambah biaya sinkronisasi ekstra pada semua operasi condition variable, terutama di lingkungan multiprosesor. Scheduler duduk di antara notifikasi dan bangun; tergantung waktu interrupt dan preemption, “thread lain berjalan sebelum thread yang mestinya dibangunkan” tidak dapat dihindari. Daripada membebankan biaya menyegel itu sepenuhnya kepada semua orang, lebih baik menerima “kadang bangun berlebih” agar condition variable tetap cepat.

Alasan kedua adalah observasi bahwa keputusan itu tidak merusak aplikasi — justru membuatnya lebih kokoh. Karena spurious wakeup diizinkan, kode yang benar selalu menulis loop yang memeriksa predikat (kondisi yang ditunggu). Rationale POSIX menyatakan bahwa memaksa loop ini membuat kode mendokumentasikan dirinya sendiri dan lebih kokoh.3 Begitu loop ada, arti notifikasi diturunkan dari “jaminan bahwa kondisi terpenuhi” menjadi “petunjuk bahwa kondisi mungkin sudah berubah”, dan menjadi tahan terhadap sedikit perubahan desain di sisi pemberi tahu (membangunkan terlalu banyak, membangunkan sekaligus, dan sebagainya).

Stolen wakeup lebih struktural lagi. Selalu ada selisih waktu antara sisi pemberi tahu memanggil WakeConditionVariable dan thread yang dibangunkan memperoleh kembali kunci lalu kembali dari wait. Jika thread ketiga dapat mengambil kunci di sela itu, ia dapat mengonsumsi kondisi (isi antrian, dan sebagainya) lebih dulu. Itu celah yang tidak bisa dihapus berapa pun implementasi dipoles, karena berasal dari bentuk alat condition variable itu sendiri.

Linimasa stolen wakeupProdusen memasukkan satu item ke antrian dan membangunkan konsumen A yang sedang menunggu, tetapi sebelum A memperoleh kembali kunci, konsumen B mengambil kunci dan mengambil satu item, sehingga antrian kosong ketika A bangunKonsumen BProdusenKonsumen A (sedang menunggu)Konsumen BProdusenKonsumen A (sedang menunggu)Bangun, tetapi menunggu memperoleh kembali kunciAntrian kosong (diserobot)Tambah satu item ke antrianWakeConditionVariableAmbil kunci dan ambil satu itemPeroleh kembali kunci dan kembali dari waitPeriksa ulang dengan while, lalu wait lagi

Gambar 2: “Stolen wakeup”, di mana thread ketiga mengonsumsi kondisi di selisih waktu antara notifikasi dan bangun, dapat terjadi pada implementasi apa pun.

Dengan kata lain, bahkan jika OS sepenuhnya mengeradikasi spurious wakeup, selama stolen wakeup ada, “bangun = kondisi terpenuhi” tidak dapat ditulis. Loop pemeriksaan ulang di sisi yang menunggu tetap wajib, dan kalau begitu, lebih menguntungkan mengizinkan spurious wakeup agar implementasi tetap cepat — itulah keputusan desain condition variable yang dipakai puluhan tahun.

4. Di lapisan mana ia muncul di Windows

Sifat ini muncul di lapisan primitif sinkronisasi Windows mana pun yang dipakai. Agar terasa bahwa tidak ada jalan lolos, tidak peduli API lapisan mana yang ditulis, kita tinjau lapisan yang mewakili.

Condition variable Win32 (CONDITION_VARIABLE), seperti sudah disebut, mencantumkan baik spurious wakeup maupun stolen wakeup di dokumentasi SleepConditionVariableCS / SleepConditionVariableSRW, dan menuntut pemeriksaan ulang predikat dengan loop while.2 Contoh pemakaian resmi (antrian produsen–konsumen) juga menulis tunggu di dalam loop while.8

WaitOnAddress yang lebih rendah adalah API tunggu yang lebih primitif daripada condition variable: “tunggu sampai nilai di alamat tertentu berubah” (Windows 8 ke atas). Bahkan API yang mendekati lapisan paling bawah ini, dokumentasinya menyatakan “dijamin kembali ketika alamat di-signal, tetapi juga diizinkan kembali karena alasan lain”, dan sebagai contoh bangun lebih awal menyebut kondisi memori rendah, pembatalan wake sebelumnya untuk alamat yang sama, dan eksekusi di checked build. Itulah sebabnya contoh pemakaian dokumentasi sendiri berbentuk “loop while yang membandingkan nilai lagi”.9

std::condition_variable di C++ sama. Dokumentasi MSVC menulis tentang wait tanpa predikat bahwa ia “memblokir sampai di-signal oleh pemanggilan notify_one / notify_all. Dapat juga bangun secara spurious”, dan menjelaskan bahwa wait(lock, pred) dengan predikat secara efektif menjalankan kode berikut.5

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

Dengan kata lain, wait dengan predikat yang direkomendasikan di C++ tidak lain adalah bentuk di mana pustaka menggantikan “bungkus dengan while” yang dijelaskan artikel ini. cppreference pun menyatakan bahwa wait tanpa predikat dapat dilepas secara spurious.4

Monitor.Wait / Pulse di .NET punya struktur antrian sendiri berupa waiting queue dan ready queue, tetapi disiplinnya tidak berubah. Thread yang dibangunkan oleh Pulse / PulseAll pindah ke ready queue, lalu kembali dari Wait menurut urutan ia berhasil memperoleh kembali kunci. Thread lain dapat mengonsumsi kondisi di sela sebelum kunci diperoleh kembali — sama seperti Win32 — dan dokumentasi pun ditulis dengan asumsi “thread yang dibangunkan mengevaluasi ulang kondisi yang membuatnya masuk tunggu, dan memanggil Wait lagi jika perlu”.610

Setiap lapisan menuntut pemeriksaan ulang predikatBaik std::condition_variable di C++, Monitor di .NET, CONDITION_VARIABLE di Win32, maupun WaitOnAddress tingkat rendah, dokumentasi resmi menuntut pemeriksaan ulang kondisi setelah bangunC++ std::condition_variableSetelah bangun, periksa ulang kondisi (while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

Gambar 3: Ganti bahasa atau kerangka, di lapisan primitif tunggu mana pun “pemeriksaan ulang setelah bangun” tetap dituntut secara resmi.

5. Cara menunggu yang benar — tulis dengan while dan predikat

Dari sini, implementasi. Prinsipnya hanya tiga.

  1. Pegang yang ditunggu sebagai state (predikat), bukan sebagai “notifikasi”. Bukan “sudah dibangunkan atau belum”, melainkan shared state yang dilindungi kunci — “apakah antrian tidak kosong”, “apakah flag sudah diangkat”.
  2. Selalu tempatkan wait di dalam loop while pada kondisi. Setiap kali bangun, periksa kondisi; jika belum terpenuhi, tidur lagi.
  3. Pembaruan dan pemeriksaan kondisi dilakukan di dalam kunci yang sama. Sisi pemberi tahu memperbarui state dulu, baru memberi tahu.
Alur loop tunggu yang benarAmbil kunci lalu periksa kondisi; jika belum terpenuhi, lepas kunci dan tidur; saat bangun peroleh kembali kunci dan kembali ke pemeriksaan kondisi. Hanya ketika terpenuhi, lanjutkan pemrosesan sambil masih memegang kunciTidakYaAmbil kunciKondisi terpenuhi?wait (lepas kunci dan tidur)Bangun (peroleh kembali kunci)Proses sambil masih memegang kunci

Gambar 4: Tunggu yang benar adalah loop, dan tidak ada celah antara pemeriksaan kondisi dan pemrosesan (keduanya dilakukan sambil memegang kunci).

Bentuk ini punya manfaat yang mudah terlewat. Pada saat keluar dari loop while, sudah dipastikan, sambil masih memegang kunci, bahwa “kondisi terpenuhi”. Loop yang menangani spurious wakeup, apa adanya, menjadi jaminan bahwa “tidak ada celah race antara pemeriksaan kondisi dan pemrosesan”.

Bentuk dasar di Win32 (C)

CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0;   // shared state yang dilindungi cs

// Inisialisasi sekali saat startup (jika inisialisasi statis, cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);

// Sisi yang menunggu (konsumen)
EnterCriticalSection(&cs);
while (queueCount == 0) {                       // selalu while, bukan if
    SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// Di sini kunci dipegang, dan queueCount > 0 dijamin
--queueCount;
LeaveCriticalSection(&cs);

// Sisi pemberi tahu (produsen)
EnterCriticalSection(&cs);
++queueCount;                                    // pembaruan state di dalam kunci
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv);                      // notifikasi boleh setelah kunci dilepas

Notifikasi (WakeConditionVariable) dapat dipanggil dari dalam maupun luar kunci, tetapi dokumentasi menyatakan bahwa biasanya lebih baik membangunkan setelah melepaskan kunci agar context switch berkurang.1 Sebaliknya, pembaruan state itu sendiri (++queueCount) selalu dilakukan di dalam kunci. Jangan mencampur dua hal ini.

Bentuk dasar di C++ — wait dengan predikat sebagai bawaan

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

// Sisi yang menunggu
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); });   // di dalam: while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();

// Sisi pemberi tahu
{
    std::lock_guard<std::mutex> lk(m);
    q.push(std::move(item));
}
cv.notify_one();

Wait dengan predikat menggantikan loop, jadi while yang ditulis tangan tidak diperlukan. Saat memperbaiki kode lama yang masih punya loop tertulis, while (q.empty()) cv.wait(lk); adalah bentuk yang benar, jadi tidak perlu buru-buru menuliskannya ulang. Yang salah hanya if (q.empty()) cv.wait(lk);.

Bentuk dasar di C#

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

// Sisi yang menunggu
lock (_gate)
{
    while (_queue.Count == 0)          // selalu while, bukan if
    {
        Monitor.Wait(_gate);
    }
    var item = _queue.Dequeue();
}

// Sisi pemberi tahu
lock (_gate)
{
    _queue.Enqueue(item);
    Monitor.Pulse(_gate);              // Pulse pada Monitor hanya boleh dipanggil di dalam kunci
}

Monitor.Wait / Pulse / PulseAll hanya boleh dipanggil dari dalam kunci (blok lock) — ini berbeda dari Win32. Memanggil dari luar kunci menghasilkan SynchronizationLockException.10

Tunggu dengan timeout — hitung sisa waktu dari tenggat

Jika menunggu dengan timeout, meneruskan “nilai timeout yang sama” setiap putaran loop membuat waktu tunggu memanjang setiap kali spurious wakeup. Bentuk yang benar: tetapkan dulu waktu tenggat, lalu hitung ulang sisa waktu.

ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
    ULONGLONG now = GetTickCount64();
    if (now >= deadline) {
        break;                          // timeout (kondisi tetap tidak terpenuhi)
    }
    if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
        GetLastError() != ERROR_TIMEOUT) {
        break;                          // kegagalan selain timeout: hentikan tunggu dan keluar
    }
    // ERROR_TIMEOUT dikonfirmasi akhir lewat kondisi while dan pemeriksaan tenggat
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
Alur tunggu dengan timeout yang benarTetapkan dulu waktu tenggat; setiap kali bangun, periksa kondisi dan tenggat; jika masih, hitung ulang sisa waktu lalu kembali menungguYaTidakYaTidakTetapkan waktu tenggatKondisi terpenuhi?Lanjut ke pemrosesanTenggat sudah lewat?Penanganan timeoutHitung sisa waktu lalu wait

Gambar 5: Tunggu dengan timeout bukan meneruskan “durasi tunggu yang sama” lagi, melainkan menghitung ulang sisa waktu berdasarkan waktu tenggat.

Di C++, perhitungan ini sendiri dapat diserahkan kepada overload wait_until (waktu absolut) + predikat. Bahkan ketika kembali karena timeout, nilai akhir predikat dikembalikan, sehingga penentuan “kehabisan waktu atau sempat terpenuhi” juga dapat dilakukan berdasarkan predikat.5

6. Kumpulan pola yang tidak boleh dilakukan

Memeriksa sekali saja dengan if. Ini tokoh utama artikel. Begitu spurious wakeup atau stolen wakeup terjadi, pemrosesan lanjut padahal kondisi belum terpenuhi. Pengambilan dari antrian kosong, akses data yang belum diinisialisasi, double-free — gejalanya menjadi “crash atau kerusakan data yang hanya muncul kadang-kadang”.

Memeriksa atau memperbarui kondisi di luar kunci. Sisi yang menunggu melihat kondisi di luar kunci, memutuskan “belum”, lalu di celah sebelum masuk wait, sisi pemberi tahu memperbarui state dan mengirim notifikasi: notifikasi ditembakkan ke condition variable tanpa waiter, lalu hilang. Sisi yang menunggu kemudian masuk wait dan terus menunggu notifikasi yang tidak akan datang lagi. Inilah lost wakeup (gagal membangunkan), cerminan spurious wakeup. API tunggu condition variable dirancang agar “melepas kunci dan beralih ke tidur secara atomik” justru untuk menutup celah ini.1 Selama disiplin kunci dijaga, ia tidak terjadi.

Linimasa lost wakeupDi celah antara sisi yang menunggu memeriksa kondisi di luar kunci dan masuk wait, sisi pemberi tahu memperbarui state dan mengirim notifikasi, notifikasi dikirim ke condition variable tanpa waiter lalu hilang, dan sisi yang menunggu terus menunggu notifikasi yang tidak akan datang lagiSisi pemberi tahuSisi yang menungguSisi pemberi tahuSisi yang menungguPada saat ini tidak ada waiterNotifikasi sudah hilang dan tidak bangunPeriksa kondisi di luar kunci (belum terpenuhi)Perbarui state dan beri tahuMasuk wait

Gambar 6: Jika kondisi diperiksa di luar kunci, notifikasi dapat lewat di celah antara pemeriksaan dan wait — lost wakeup.

Meniru “notifikasi transien” condition variable dengan pulse pada event. Event (CreateEvent + SetEvent) itu sendiri bukan antipola. Sinyal bangun untuk konfigurasi di mana satu konsumen memproses antrian sampai kosong, atau perintah berhenti yang sekali diangkat tidak diturunkan (manual-reset event), adalah tempat yang tepat memakai event. Jika ingin menggabungkan dengan sasaran tunggu lain lewat WaitForMultipleObjects, atau menyeberangi batas proses, condition variable — objek user-mode yang tidak dapat dibagi antar proses — justru tidak dapat dipakai.1 Yang berbahaya adalah mencoba meniru, dengan operasi event, notifikasi transien condition variable yang “hanya membangunkan thread yang sedang menunggu pada saat itu, tanpa meninggalkan state”. Gagasan itu hampir pasti berujung pada PulseEvent berikut.

Memakai PulseEvent. API yang, pada manual-reset event, “membangunkan semua yang sedang menunggu sekarang, lalu segera mengembalikan ke nonsignaled”. Microsoft sendiri menyatakan di dokumentasi: “fungsi ini tidak andal dan tidak boleh dipakai. Ada terutama untuk kompatibilitas mundur. Pakai condition variable sebagai gantinya.” Alasannya: thread yang sedang menunggu dapat dikeluarkan sementara dari keadaan tunggu oleh APC mode kernel, lalu kembali ke tunggu setelah APC selesai. Jika PulseEvent dipanggil di sekejap itu, thread itu tidak termasuk “yang sedang menunggu pada saat pemanggilan”, jadi tidak dibangunkan.7 Kernel APC dipakai OS secara internal dan tidak dapat dikendalikan dari sisi aplikasi.11 Masalah ini juga menjadi peringatan analisis statis (C28648).12 Jika spurious wakeup adalah masalah “bangun berlebih”, yang ini adalah masalah “mestinya bangun tetapi ketiduran”, dan loop while pun tidak menyelamatkan — karena notifikasinya sendiri hilang.

Mengirim notifikasi dulu tanpa memegang kunci, sebelum memperbarui state. Memanggil WakeConditionVariable ketika state masih lama, lalu baru mengambil kunci dan memperbarui state — urutan itu membuat thread yang dibangunkan, pada saat memeriksa kondisi, masih melihat kondisi belum terpenuhi, lalu tidur lagi. Jika notifikasi berikutnya tidak datang, ia tetap tidur. Catatan: jika ditulis dalam urutan “notifikasi → pembaruan → pelepasan” sambil tetap memegang kunci yang sama, sisi yang menunggu tidak dapat memeriksa kondisi sampai memperoleh kembali kunci, jadi tidak ada kerugian nyata. Meskipun begitu, agar pembaca tidak harus memverifikasi syarat aman ini setiap kali, lebih aman menyeragamkan urutan “perbarui state di dalam kunci, baru beri tahu setelah itu”.

7. Cara menelusuri ketika menjumpainya

Bug terkait spurious wakeup khas “hanya muncul jarang”. Dibalik dari gejala, ia terbagi dua jalur.

Jalur 1: pemrosesan lanjut padahal kondisi belum terpenuhi. Pengecualian atau crash karena pengambilan dari antrian kosong, hasil pemrosesan yang hilang, dan sebagainya. Yang dicurigai adalah tunggu tanpa predikat. Di code review, ini dapat disaring secara mekanis — cari tempat argumen cv.wait( hanya satu, dan tempat SleepConditionVariableCS / Monitor.Wait dibungkus if bukan while. Pemeriksaan ini tidak perlu menunggu reproduksi; ini langkah dengan rasio biaya-manfaat tertinggi.

Jalur 2: thread yang mestinya bangun tidak bangun (hang). Yang dicurigai adalah lost wakeup (pemeriksaan kondisi di luar kunci, notifikasi di luar kunci sebelum pembaruan state) dan PulseEvent. Ambil dump dari proses yang sedang hang, lalu lihat stack setiap thread: thread mana yang berhenti di API tunggu mana dapat diidentifikasi. Setelah itu, telusuri dari kode “notifikasi itu mestinya dikirim siapa, dalam urutan apa”.

Alur pemisahan dari gejalaJika gejalanya pemrosesan lanjut padahal kondisi belum terpenuhi, saring tunggu tanpa predikat lewat pencarian kode; jika gejalanya thread tidak bangun, identifikasi lokasi tunggu dari dump lalu curigai lost wakeup dan PulseEventBug yang hanya muncul jarangPemrosesan lanjut padahal kondisi belum terpenuhiThread yang mestinya bangun tidak bangunCari tunggu tanpa predikat di kodeIdentifikasi thread yang sedang menunggu dari dumpUbah if menjadi while atau wait dengan predikatCurigai lost wakeup dan PulseEvent

Gambar 7: Apakah gejalanya “terlalu maju” atau “tidak bangun” memisahkan lokasi yang dicurigai dan cara investigasi.

Jika ingin mereproduksi, kaidahnya adalah memperlebar jendela race. Perbanyak thread di atas jumlah core fisik, sisipkan Sleep yang disengaja antara tunggu dan notifikasi, jalankan baik debug build maupun release build — operasi semacam itu menambah sebaran timing. Ketika mengonfirmasi bahwa “setelah memperbaiki wait tanpa predikat, tidak lagi tereproduksi”, bandingkan dengan stres yang sama.

8. Ringkasan — daftar periksa

  • Jalur kembali dari wait ada tiga: “notifikasi yang sah, spurious wakeup, stolen wakeup”, dan pemanggil tidak dapat membedakannya. Karena itu, tunggu selalu ditulis dengan loop while pada kondisi.
  • Spurious wakeup adalah spesifikasi yang Win32, C++, dan POSIX izinkan dengan sengaja sebagai trade-off terhadap performa; ia tidak hilang lewat perbaikan OS atau pindah pustaka. Pada Monitor.Wait di .NET, bangun tanpa alasan tidak diasumsikan, tetapi selama ada stolen wakeup dan timeout, disiplin while yang sama tetap diperlukan.
  • Di C++, jadikan wait(lock, pred) dengan predikat sebagai bawaan. Loop digantikan pustaka.
  • Pembaruan dan pemeriksaan kondisi di dalam kunci yang sama. Notifikasi dikirim “setelah memperbarui state”. Notifikasi Win32/C++ boleh setelah kunci dilepas, tetapi Pulse di C# hanya di dalam kunci.
  • Tunggu dengan timeout: tetapkan waktu tenggat, lalu hitung ulang sisa waktu. Di C++, wait_until + predikat.
  • Jangan meniru notifikasi transien condition variable dengan pulse pada event. Khususnya PulseEvent, dokumentasi resmi menyatakan tegas “jangan dipakai, pakai condition variable sebagai gantinya”. Event itu sendiri masih alat yang benar untuk perintah berhenti, penggabungan dengan WaitForMultipleObjects, dan sinkronisasi antar proses.
  • Di review, cari secara mekanis “wait tanpa predikat” dan “if + wait”. Bug yang hanya muncul jarang dapat diberantas tanpa menunggu reproduksi.

Spurious wakeup, bertolak dari keanehan namanya, penanganannya terkumpul pada satu kata kunci — mengubah if menjadi while. Dan di belakang satu baris itu terbentang ide desain alat condition variable: “notifikasi yang tepat mahal, jadi pemeriksaan menjadi tanggung jawab sisi yang menunggu”. Jika dipahami beserta mekanismenya, disiplin yang sama dapat diterapkan tanpa ragu meskipun bahasa atau kerangka berubah.

Artikel terkait

Area konsultasi terkait

Di Komura Soft (合同会社小村ソフト), kami menangani review desain kode multithreading, investigasi penyebab crash/hang yang “hampir tidak pernah bisa direproduksi” (analisis dump), dan perbaikan kode sinkronisasi lama (yang bergantung pada event, PulseEvent, dan sebagainya) ke basis condition variable. Cukup mulai dari pemisahan gejala; silakan berkonsultasi.

Tautan referensi

  1. Microsoft Learn, Condition Variables. Condition variable adalah objek user-mode yang melepas kunci dan beralih ke tunggu secara atomik; ada spurious wakeup (bangun yang tidak terikat pada bangun eksplisit) dan stolen wakeup (thread lain berjalan sebelum thread yang dibangunkan), sehingga setelah kembali dari tunggu predikat harus diperiksa ulang dengan loop while; notifikasi dapat dilakukan dari dalam maupun luar kunci, tetapi untuk mengurangi context switch lebih baik membangunkan setelah kunci dilepas. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). Melepas critical section yang ditentukan dan menunggu di condition variable secara atomik; thread yang bangun memperoleh kembali critical section sebelum kembali; pada timeout dikembalikan ERROR_TIMEOUT; ada spurious wakeup dan stolen wakeup, sehingga setelah kembali dari tunggu predikat harus diperiksa ulang (biasanya dengan loop while). ↩ ↩2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. Spurious wakeup dari pthread_cond_wait / pthread_cond_timedwait dapat terjadi; kembali dari wait tidak berarti apa-apa tentang nilai predikat, jadi predikat harus dievaluasi ulang; di Rationale dinyatakan bahwa implementasi “membangunkan persis satu” dapat memperlambat operasi condition variable terutama di multiprosesor, dan mengizinkan spurious wakeup memaksa loop pemeriksaan predikat sehingga membuat aplikasi lebih kokoh. ↩ ↩2 ↩3

  4. cppreference.com, std::condition_variable::wait. Wait tanpa predikat dapat dilepas oleh spurious wakeup; overload dengan predikat setara dengan while (!pred()) wait(lock); didefinisikan sebagai loop yang setiap kali notifikasi atau spurious wakeup memperoleh kembali kunci lalu memeriksa predikat. ↩ ↩2

  5. Microsoft Learn, condition_variable Class. Wait tanpa predikat dilepas oleh notify_one / notify_all dan juga dapat bangun secara spurious; wait(lock, pred) dengan predikat secara efektif menjalankan while (!Pred()) wait(Lck); wait_for / wait_until punya sifat yang sama dan overload dengan predikat. ↩ ↩2 ↩3

  6. Microsoft Learn, Monitor.Wait Method. Wait melepas kunci dan masuk waiting queue; setelah dibangunkan oleh Pulse / PulseAll, tidak kembali sampai memperoleh kembali kunci; pemakaian yang diasumsikan adalah thread yang dibangunkan mengevaluasi ulang kondisi yang membuatnya masuk tunggu, dan memanggil Wait lagi jika perlu. ↩ ↩2

  7. Microsoft Learn, PulseEvent function (winbase.h). Thread yang sedang menunggu dapat dikeluarkan sementara dari keadaan tunggu oleh APC mode kernel lalu kembali setelah APC selesai; jika PulseEvent dipanggil di sela itu, thread itu tidak dilepas; karena itu PulseEvent tidak andal dan tidak boleh dipakai di aplikasi baru, dan condition variable harus dipakai sebagai gantinya. ↩ ↩2

  8. Microsoft Learn, Using Condition Variables. Sampel resmi yang mengimplementasikan antrian produsen–konsumen dengan satu critical section dan dua condition variable (BufferNotEmpty dan BufferNotFull). Tunggu dilakukan di dalam loop yang memeriksa predikat. ↩

  9. Microsoft Learn, WaitOnAddress function (synchapi.h). Fungsi yang menunggu nilai alamat berubah dijamin kembali saat di-signal, tetapi juga diizinkan kembali karena alasan lain; sebagai contoh bangun lebih awal disebut kondisi memori rendah, pembatalan wake sebelumnya ke alamat yang sama, dan eksekusi di checked build; karena itu setelah kembali nilai harus dibandingkan lagi, dan sampel resmi sendiri adalah loop while. ↩

  10. Microsoft Learn, Monitor.PulseAll Method. PulseAll memindahkan thread di waiting queue ke ready queue; ketika kunci dilepas, thread berikutnya di ready queue mengambil kunci; Pulse / PulseAll / Wait hanya boleh dipanggil dari dalam synchronization block. ↩ ↩2

  11. Microsoft Learn, Waits and APCs. Kernel APC dijalankan secara preemptive; sistem secara internal menangguhkan dan melanjutkan tunggu tanpa mengembalikan API tunggu; di sela itu sinyal transien seperti KePulseEvent dapat terlewat. ↩

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. Analisis statis memperingatkan pemakaian PulseEvent; thread yang keluar dari tunggu karena APC tidak dilepas dan dapat hang selamanya; pedoman penggantian ke SetEvent atau objek sinkronisasi lain. ↩

Artikel terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.

Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.

Artikel ini berkaitan langsung dengan layanan berikut.

Pertanyaan yang sering diajukan

Pertanyaan yang sering muncul dalam konsultasi tentang topik artikel ini.

Apakah spurious wakeup bug di OS atau pustaka?
Bukan bug, melainkan perilaku yang sudah tertulis di spesifikasi. SleepConditionVariableCS di Win32, std::condition_variable di C++, maupun pthread_cond_wait di POSIX — semuanya dicantumkan secara eksplisit di dokumentasi resmi atau standar bahwa "bangun yang tidak terikat pada notifikasi dapat terjadi". Implementasi yang melarangnya secara teoretis mungkin, tetapi semua operasi condition variable (terutama notifikasi di lingkungan multiprosesor) akan menjadi lebih lambat, sehingga perilaku itu ditoleransi dengan keputusan bahwa "kebenaran tetap terjaga asal sisi yang menunggu memeriksa ulang kondisi". Jadi penanganannya bukan menunggu perbaikan di sisi OS, melainkan selalu menulis wait di dalam loop while (atau wait dengan predikat).
Apakah membungkus wait dengan while menurunkan performa?
Dalam praktik, biayanya dapat diabaikan. Yang bertambah jika wait dibungkus while hanyalah satu pemeriksaan kondisi setiap kali bangun — perbandingan ringan sambil masih memegang kunci. Spurious wakeup sendiri jarang terjadi, jadi loop ekstra hanya berputar di situasi luar biasa. Sebaliknya, biaya membiarkan pemeriksaan sebagai if adalah "bug yang hampir tidak pernah bisa direproduksi" di mana pemrosesan lanjut padahal kondisi belum terpenuhi — tidak ada bandingannya. Yang benar-benar menentukan biaya tunggu condition variable adalah contention pada kunci dan seberapa sering notifikasi dikirim, bukan ada-tidaknya while.
Jika memakai wait dengan predikat di C++, apakah spurious wakeup tidak perlu dipikirkan lagi?
Untuk loop tunggu, ya: cv.wait(lock, pred) secara efektif menjalankan while (!pred()) wait(lock); sehingga baik spurious wakeup maupun stolen wakeup terserap otomatis. Kode C++ baru sebaiknya memakai overload predikat sebagai bentuk bawaan. Namun, pembaruan shared state yang dirujuk predikat tetap harus dilindungi mutex yang sama, dan sisi pemberi tahu tetap harus memperbarui state dulu baru memanggil notify. Wait dengan predikat hanya menggantikan loop; ia tidak menggantikan disiplin kunci.
Apakah masalah yang sama juga terjadi pada Monitor.Wait di C#?
Terjadi. Thread yang menunggu di Monitor.Wait dibangunkan oleh Pulse/PulseAll, lalu memperoleh kembali kunci sebelum keluar dari Wait; di sela itu, thread lain mungkin sudah mengambil kunci lebih dulu dan mengonsumsi kondisi (stolen wakeup). Dokumentasi Microsoft pun ditulis dengan asumsi bahwa thread yang dibangunkan mengevaluasi ulang kondisi yang membuatnya masuk tunggu, dan memanggil Wait lagi jika perlu. Jadi di C# pun bentuk dasarnya adalah while (!kondisi) Monitor.Wait(gate);. Kendala yang berbeda dari Win32: Wait/Pulse hanya boleh dipanggil dari dalam pernyataan lock.
Apakah spurious wakeup juga ada jika menunggu event dengan WaitForSingleObject?
Pada tunggu biasa (non-alertable), WAIT_OBJECT_0 dikembalikan hanya ketika objek benar-benar beralih ke signaled; "bangun tanpa alasan" seperti pada condition variable tidak terjadi. Namun "event menjadi signaled" dan "kondisi aplikasi sendiri terpenuhi" adalah dua hal berbeda. Jika beberapa konsumen dibangunkan oleh event yang sama, thread yang mengambil kunci lebih dulu mengonsumsi kondisi, sehingga pada akhirnya pemeriksaan kondisi setelah bangun tetap diperlukan. Selain itu, desain yang mencoba meniru notifikasi transien condition variable — "hanya membangunkan yang sedang menunggu pada saat itu" — dengan event cenderung berujung pada masalah keandalan PulseEvent, jadi untuk menunggu terpenuhinya state di dalam proses, condition variable lebih aman.

Profil penulis

Halaman perkenalan penulis artikel.

Go Komura

Direktur KomuraSoft LLC

Berspesialisasi dalam pengembangan perangkat lunak Windows, konsultasi teknis, dan investigasi bug, terutama pada proyek dengan sistem yang sudah ada dan bug yang sulit direproduksi.

Kembali ke blog