Apa itu condition variable?
Condition variable memungkinkan sebuah thread menunggu sampai kondisi tertentu terpenuhi, tanpa harus terus-menerus mengecek kondisi itu (busy-waiting). Selalu dipakai berpasangan dengan mutex — mutex melindungi data yang dicek, condition variable menangani bagian “tunggu sampai data itu berubah sesuai yang diharapkan”.
Mutex menjawab “siapa yang boleh masuk sekarang?”. Condition variable menjawab pertanyaan yang lebih spesifik: “boleh masuk, tapi tunggu dulu sampai kondisi X benar.”
Ilustrasi
Tanpa condition variable (busy-waiting, boros CPU):
Consumer: LOCK, cek kosong? ya, UNLOCK — diulang terus dalam loop
Dengan condition variable (tidur sampai dibangunkan):
Consumer: LOCK → kosong? ya → cond_wait (tidur, lepas mutex)
Producer: LOCK → isi buffer → cond_signal
Consumer: (dibangunkan, lock ulang otomatis) → ambil item → UNLOCK
Kenapa diperlukan
Mutex saja cukup untuk melindungi shared data, tapi tidak cukup kalau sebuah thread harus menunggu sampai kondisi tertentu berubah — misalnya menunggu buffer terisi sebelum bisa mengambil item. Kalau dipaksa pakai mutex saja, satu-satunya cara adalah polling terus-menerus (lock, cek, unlock, ulangi), yang membuang CPU cycle sia-sia.
Studi kasus klasiknya: producer-consumer dengan bounded buffer. Producer menaruh item ke buffer, consumer mengambilnya. Kalau buffer penuh, producer harus menunggu sampai ada slot kosong. Kalau buffer kosong, consumer harus menunggu sampai ada item baru. Condition variable memungkinkan keduanya tidur (tidak memakan CPU) sampai kondisi yang mereka tunggu benar-benar berubah, lalu dibangunkan secara eksplisit oleh pihak lain.
Implementasi
Struktur proyek
.
├── main.c
├── buffer.c
└── buffer.h
Step 1: interface buffer
#ifndef BUFFER_H
#define BUFFER_H
#define KAPASITAS 5
void buffer_init(void);
void produce(int item);
int consume(void);
#endif
Step 2: buffer dengan mutex + condition variable
Circular buffer dilindungi mutex, dengan dua condition variable: satu untuk “tidak penuh”, satu untuk “tidak kosong”.
#include "buffer.h"
#include <pthread.h>
#include <stdio.h>
static int buffer[KAPASITAS];
static int count = 0;
static int in = 0, out = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void buffer_init(void)
{
count = 0;
in = 0;
out = 0;
}
void produce(int item)
{
pthread_mutex_lock(&mutex);
while (count == KAPASITAS) { // while, bukan if — lihat penjelasan
printf("Buffer penuh, producer menunggu...\n");
pthread_cond_wait(¬_full, &mutex);
}
buffer[in] = item;
in = (in + 1) % KAPASITAS;
count++;
printf("Produce: %d (count=%d)\n", item, count);
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mutex);
}
int consume(void)
{
pthread_mutex_lock(&mutex);
while (count == 0) {
printf("Buffer kosong, consumer menunggu...\n");
pthread_cond_wait(¬_empty, &mutex);
}
int item = buffer[out];
out = (out + 1) % KAPASITAS;
count--;
printf("Consume: %d (count=%d)\n", item, count);
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
return item;
}
not_full dan not_empty sengaja dipisah karena mewakili kondisi berbeda. Bisa saja digabung jadi satu cond var dengan broadcast, tapi kurang efisien karena thread yang dibangunkan bisa jadi menunggu kondisi yang salah.
Step 3: producer dan consumer
#include "buffer.h"
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *producer(void *arg)
{
int id = *(int *)arg;
for (int i = 0; i < 5; i++) {
int item = id * 100 + i;
produce(item);
usleep(100000);
}
return NULL;
}
void *consumer(void *arg)
{
for (int i = 0; i < 5; i++) {
consume();
usleep(150000);
}
return NULL;
}
int main(void)
{
buffer_init();
pthread_t producers[2], consumers[2];
int ids[2] = {1, 2};
for (int i = 0; i < 2; i++) {
pthread_create(&producers[i], NULL, producer, &ids[i]);
pthread_create(&consumers[i], NULL, consumer, NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(producers[i], NULL);
pthread_join(consumers[i], NULL);
}
return 0;
}
Penjelasan
pthread_cond_wait(¬_full, &mutex) melepas mutex secara atomik dan menidurkan thread sampai dibangunkan1. Saat dibangunkan, mutex otomatis di-lock kembali sebelum fungsi return. Kalau ini dilakukan manual (unlock lalu wait terpisah), ada celah race condition di antara keduanya.
Kenapa harus while, bukan if: POSIX mengizinkan spurious wakeup, thread bisa terbangun dari cond_wait tanpa ada signal/broadcast sama sekali. Pakai if berarti thread bisa lanjut jalan padahal kondisinya belum benar-benar terpenuhi. while memastikan kondisi dicek ulang setiap kali bangun.
pthread_cond_signal membangunkan satu thread yang menunggu (tidak dijamin yang mana). pthread_cond_broadcast membangunkan semua. Kalau ada beberapa thread menunggu kondisi berbeda-beda di cond var yang sama, broadcast lebih aman, walau ada thread yang bangun sia-sia dan tidur lagi.
Sama seperti mutex, pthread_cond_destroy dan pthread_mutex_destroy harus dipanggil setelah tidak dipakai lagi.
Timeline eksekusi
| Time | Producer1 | Consumer1 | Buffer (count) |
|---|---|---|---|
| t0 | LOCK | (nunggu lock) | 0 |
| t1 | count==0, lanjut | (nunggu lock) | 0 |
| t2 | produce item | 1 | |
| t3 | signal not_empty | (masih nunggu) | 1 |
| t4 | UNLOCK | 1 | |
| t5 | LOCK | 1 | |
| t6 | count>0, ambil item | 0 | |
| t7 | signal not_full | 0 | |
| t8 | UNLOCK | 0 |
Kompilasi
gcc -o buffer main.c buffer.c -lpthread
./buffer
Output (urutan bisa bervariasi):
Produce: 100 (count=1)
Consume: 100 (count=0)
Produce: 200 (count=1)
Produce: 101 (count=2)
Consume: 200 (count=1)
...
Best practices
Selalu while, jangan if, untuk mengantisipasi spurious wakeup. Pakai cond var terpisah untuk kondisi yang berbeda, lebih efisien daripada satu cond var untuk semua. Signal/broadcast di dalam lock lebih konsisten dan gampang dinalar, walau POSIX sebetulnya mengizinkan di luar lock. Destroy semua cond var dan mutex setelah selesai, dan jangan lupa: cond_wait melepas mutex otomatis saat tidur, tapi kode setelahnya tetap harus unlock secara eksplisit.
Kesimpulan
Condition variable melengkapi mutex untuk kasus di mana thread perlu menunggu kondisi tertentu, bukan cuma menunggu giliran akses. Polanya: lock, cek kondisi dalam while, cond_wait kalau belum terpenuhi, lanjut, signal/broadcast, unlock.
Footnotes
-
pthread_cond_waitdanpthread_cond_timedwaitadalah dua varian utama — yang kedua memungkinkan timeout supaya thread tidak menunggu selamanya kalau signal tidak pernah datang. ↩