Mutex menyelesaikan persaingan data, tetapi ada kebutuhan lain yang lebih halus: thread harus bisa MENUNGGU sampai sesuatu terjadi — data baru masuk antrean, sinyal selesai dipancarkan — tanpa memeriksa berulang-ulang sambil membakar CPU. Alat standar untuk itu adalah std::condition_variable: mekanisme bagi thread untuk tidur dan dibangunkan oleh thread lain lewat notify_. Bersamaannya kita juga bertemu konsep penting yang sering mengejutkan pemula: spurious wakeup — bangun tanpa alasan yang kelihatan — dan kenapa wait selalu dipasangkan dengan predicate. Chapter ini mengungkap semuanya lewat pola klasik producer-consumer yang keluarannya bisa kita jaga tetap deterministik.
Masalah: Menunggu Tanpa Busy Waiting #
Cara naif menunggu data adalah loop yang memeriksa terus-menerus:
while (antrean.empty()) { } // busy waiting — membakar CPU tanpa hasil
Loop ini legal dari sisi kebenaran (bila dilindungi mutex yang benar) tapi boros: thread yang menunggu terus dipindahkan oleh scheduler dan memakan jatah CPU yang seharusnya dipakai thread lain. condition_variable membalik caranya: thread tidur total (state Blocked), dan sistem membangunkannya hanya saat ada notify. Selama tidur, thread tidak memakai CPU sama sekali.
wait dan Predicate: Perlindungan dari Spurious Wakeup #
Bentuk wait yang kita pakai selalu dua argumen:
siap.wait(kunci, [] { return !antrean.empty(); });Perilakunya: bila predicate sudah true, lanjut segera; bila belum, lepas kunci lalu tidur — melepas kunci adalah kunci agar produsen bisa masuk dan mengisi antrean. Saat dibangunkan, kunci diambil kembali dan predicate diperiksa ulang; loop ini berlanjut sampai predicate true.
Mengapa predicate wajib, bukan sekadar pemanis? Karena spurious wakeup: sistem operasi dan pustaka boleh membangunkan thread yang sedang wait tanpa ada notify yang menjelaskannya. Standar secara eksplisit mengizinkan kejadian itu, jadi thread yang bangun TIDAK boleh langsung memercayai bahwa “kondisi yang kutunggu pasti sudah tercapai” — ia harus memeriksa ulang lewat predicate. Ada pula sumber bangun yang sah: notify_all membangun semua thread penunggu, dan thread tercepat bisa menghabiskan item sebelum thread lain sempat memeriksa — yang terakhir pun harus kembali tidur. Predicate menangani kedua situasi dengan satu mekanisme yang sama.
Producer-Consumer: Pola Klasik yang Teruji #
Pola producer-consumer memisahkan pembuatan data (produsen) dari pemrosesannya (konsumen), dengan antrean di tengah. Berikut versi lengkap yang teruji: produsen mengirim 5 item, konsumen mengambil semuanya, dan pencetakan dilakukan main() setelah keduanya di-join().
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
std::queue<int> antrean;
std::mutex mAntrean;
std::condition_variable siap;
bool produksiSelesai = false;
void produsen(int jumlah) {
for (int i = 1; i <= jumlah; ++i) {
{
std::lock_guard<std::mutex> kunci(mAntrean);
antrean.push(i * 10);
} // kunci dilepas SEBELUM notify
siap.notify_one();
}
{
std::lock_guard<std::mutex> kunci(mAntrean);
produksiSelesai = true;
}
siap.notify_all(); // bangunkan semua konsumen untuk melihat tanda selesai
}
void konsumen(std::vector<int>& hasil) {
while (true) {
std::unique_lock<std::mutex> kunci(mAntrean);
// predicate: tunggu hanya selama belum ada kerja DAN produksi berjalan
siap.wait(kunci, [] {
return !antrean.empty() || produksiSelesai;
});
while (!antrean.empty()) {
hasil.push_back(antrean.front());
antrean.pop();
}
if (produksiSelesai) {
break; // antrean kosong dan tidak akan ada item lagi
}
}
}
int main() {
std::vector<int> hasil;
std::thread p(produsen, 5);
std::thread k(konsumen, std::ref(hasil));
p.join();
k.join(); // semua pekerjaan selesai -> output deterministik
std::cout << "jumlah item: " << hasil.size() << "\n";
for (int v : hasil) {
std::cout << "item: " << v << "\n";
}
return 0;
}Output yang Dihasilkan:
jumlah item: 5
item: 10
item: 20
item: 30
item: 40
item: 50Keluaran ini deterministik meski programnya konkuren, karena satu konsumen mengambil antrean secara FIFO (10, 20, 30, 40, 50 selalu dalam urutan itu) dan pencetakan dilakukan setelah kedua join(). Bedah mekanismenya:
- Baris 16-18 (push lalu tutup blok): item masuk antrean di bawah kunci, dan kunci dilepas SEBELUM
notify_one()di baris 19. Jika notify dilakukan sambil masih memegang kunci, konsumen yang bangun langsung berhadapan dengan kunci terkunci — tidak salah, tapi sia-sia. - Baris 19 (
notify_one()): membangunkan satu thread penunggu. Bila konsumen sedang tidak tidur (misalnya sedang memproses), sinyalnya tidak tersimpan — dan itulah kenapa konsumen memeriksa antrean lewat predicate, bukan memercayai sinyal. - Baris 23-25 (tanda selesai):
produksiSelesaiditulis di bawah kunci lalu disebar dengannotify_all(). Tanpa tanda ini, konsumen tidak pernah tahu kapan boleh berhenti. - Baris 30-34 (
waitdengan predicate): konsumen tidur selama antrean kosong dan produksi belum selesai. Predicate dua-kondisi ini menghindari dua kegagalan klasik: berhenti terlalu dini saat masih ada item, dan tidur selamanya setelah produksi selesai. - Baris 39-41 (
break): loop berakhir hanya bila antrean kosong DAN tanda selesai berdiri — bacaan item terakhir tidak pernah hilang. - Baris 50-51 (
join()):main()menunggu keduanya; seluruh datahasilsudah final sebelum dicetak.
notify_one atau notify_all #
Aturan praktisnya mengikuti makna peristiwa: bila peristiwa memenuhi kebutuhan SATU thread (satu item baru, satu pekerjaan), pakai notify_one — membangun semua penunggu hanya untuk satu item menyebabkan stampede lalu kembali tidur (thrashing). Bila peristiwa mengubah keadaan global yang relevan bagi semua (tanda selesai, perubahan konfigurasi), pakai notify_all — memilih notify_one di sini berisiko sebagian thread tidak pernah tahu dan tidur selamanya.
Condition variable menuntaskan sinkronisasi berbasis kunci. Namun untuk penghitung sederhana, memasang mutex di tiap penambahan terasa berat — di sanalah operasi atomik masuk. Chapter berikutnya membahas std::atomic.
Referensi: cppreference — std::condition_variable , cppreference — std::condition_variable::wait .