Spurious wakeup — mengapa condition variable "bangun tanpa notifikasi" dan cara menunggu yang benar di Windows
· Diperbarui pada: · Go Komura · 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
waitpada 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
iflaluwaittampak 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 menjalankanwhile (!pred()) wait(lock);. Itu bentuk bawaan untuk kode baru.5 Monitor.Waitdi C# membutuhkan disiplin yang sama. Kondisi dapat dikonsumsi di sela antara dibangunkan dan memperoleh kembali kunci, jadi periksa kondisi denganwhilelalu kembali keWait.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 |
flowchart TB
accTitle: Tiga kasus kembali dari wait
accDescr: wait 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 kondisi
w["Kembali dari wait"] --> a["Notifikasi yang sah"]
w --> b["Spurious wakeup (tanpa notifikasi)"]
w --> c["Stolen wakeup (kondisi sudah dikonsumsi)"]
a --> r["Periksa ulang kondisi, baru lanjut"]
b --> r
c --> r
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.
sequenceDiagram
accTitle: Linimasa stolen wakeup
accDescr: Produsen 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 bangun
participant A as Konsumen A (sedang menunggu)
participant P as Produsen
participant B as Konsumen B
P->>P: Tambah satu item ke antrian
P->>A: WakeConditionVariable
Note over A: Bangun, tetapi menunggu memperoleh kembali kunci
B->>B: Ambil kunci dan ambil satu item
A->>A: Peroleh kembali kunci dan kembali dari wait
Note over A: Antrian kosong (diserobot)
A->>A: Periksa 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
flowchart TB
accTitle: Setiap lapisan menuntut pemeriksaan ulang predikat
accDescr: Baik std::condition_variable di C++, Monitor di .NET, CONDITION_VARIABLE di Win32, maupun WaitOnAddress tingkat rendah, dokumentasi resmi menuntut pemeriksaan ulang kondisi setelah bangun
cpp["C++ std::condition_variable"] --> rule["Setelah bangun, periksa ulang kondisi (while)"]
net[".NET Monitor.Wait"] --> rule
win["Win32 CONDITION_VARIABLE"] --> rule
woa["WaitOnAddress"] --> rule
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.
- 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”.
- Selalu tempatkan
waitdi dalam loop while pada kondisi. Setiap kali bangun, periksa kondisi; jika belum terpenuhi, tidur lagi. - Pembaruan dan pemeriksaan kondisi dilakukan di dalam kunci yang sama. Sisi pemberi tahu memperbarui state dulu, baru memberi tahu.
flowchart TB
accTitle: Alur loop tunggu yang benar
accDescr: Ambil 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 kunci
l["Ambil kunci"] --> c{"Kondisi terpenuhi?"}
c -->|"Tidak"| s["wait (lepas kunci dan tidur)"]
s --> wk["Bangun (peroleh kembali kunci)"]
wk --> c
c -->|"Ya"| go["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);
flowchart TB
accTitle: Alur tunggu dengan timeout yang benar
accDescr: Tetapkan dulu waktu tenggat; setiap kali bangun, periksa kondisi dan tenggat; jika masih, hitung ulang sisa waktu lalu kembali menunggu
d["Tetapkan waktu tenggat"] --> c{"Kondisi terpenuhi?"}
c -->|"Ya"| go["Lanjut ke pemrosesan"]
c -->|"Tidak"| t{"Tenggat sudah lewat?"}
t -->|"Ya"| to["Penanganan timeout"]
t -->|"Tidak"| w["Hitung sisa waktu lalu wait"]
w --> c
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.
sequenceDiagram
accTitle: Linimasa lost wakeup
accDescr: Di 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 lagi
participant W as Sisi yang menunggu
participant N as Sisi pemberi tahu
W->>W: Periksa kondisi di luar kunci (belum terpenuhi)
N->>N: Perbarui state dan beri tahu
Note over N: Pada saat ini tidak ada waiter
W->>W: Masuk wait
Note over W: Notifikasi sudah hilang dan tidak bangun
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”.
flowchart TB
accTitle: Alur pemisahan dari gejala
accDescr: Jika 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 PulseEvent
s["Bug yang hanya muncul jarang"] --> a["Pemrosesan lanjut padahal kondisi belum terpenuhi"]
s --> b["Thread yang mestinya bangun tidak bangun"]
a --> a1["Cari tunggu tanpa predikat di kode"]
b --> b1["Identifikasi thread yang sedang menunggu dari dump"]
a1 -.-> a2["Ubah if menjadi while atau wait dengan predikat"]
b1 -.-> b2["Curigai 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
waitada 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.Waitdi .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
Pulsedi 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 denganWaitForMultipleObjects, 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
- Praktik terbaik multithreading di lapangan, edisi C++
- Praktik terbaik multithreading di lapangan, edisi C
- Praktik terbaik multithreading di lapangan, edisi .NET
- Mengapa di Windows event wait harus diutamakan daripada Sleep(1)
- Jebakan shared memory dan praktik terbaik di lapangan
- Lapisan dalam I/O Windows (bagian 2) — I/O sinkron dan asinkron: arti sebenarnya OVERLAPPED
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.
- Konsultasi teknis dan review desain
- Investigasi bug dan analisis penyebab
- Pengembangan aplikasi Windows
- Kontak
Tautan referensi
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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. ↩
-
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. ↩
-
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
-
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. ↩
-
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 terkait
Artikel terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.
API thread pool Win32 — konkurensi tanpa membuat thread, lewat CreateThreadpoolWork
Apakah kode native masih menumpuk CreateThread? Artikel ini menjelaskan API thread pool Win32 yang didesain ulang di Vista: empat objek w...
Named pipe dalam praktik — IPC andalan Windows, dari desain sampai keamanan
Penjelasan praktis named pipe, IPC andalan di Windows. Dari sumber primer: memilih mode byte versus mode pesan, desain server yang menang...
DllMain dan loader lock — alasan sebenarnya di balik peringatan "jangan lakukan apa pun saat inisialisasi DLL"
Mengapa LoadLibrary dan sinkronisasi antar-thread dilarang di dalam DllMain. Dari mekanisme loader lock yang menserialisasi semua notifik...
Praktik terbaik multithreading di lapangan: edisi C — menulis aman mengikuti cara Win32 API
Pola mapan multithreading C × Win32 adalah pembuatan thread dengan _beginthreadex, kunci SRW dan variabel kondisi, Interlocked, serta des...
Praktik terbaik multithreading di lapangan: edisi C++ — menghilangkan kecelakaan secara struktural dengan RAII dan jthread
Multithreading di C++ adalah dunia di mana data race menjadi perilaku tak terdefinisi. Artikel ini merapikan jebakan destruktor std::thre...
Topik terkait
Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.
Topik teknis Windows
Portal tentang pengembangan Windows, investigasi bug, dan pemanfaatan aset yang ada.
Layanan yang terkait dengan topik ini
Artikel ini berkaitan langsung dengan layanan berikut.
Pengembangan aplikasi Windows
Aplikasi bisnis, integrasi perangkat, dan alat komunikasi, dari kebutuhan hingga pengembangan.
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.