Mutex memecahkan masalah akses bersama, tetapi tidak efisien ketika thread harus menunggu kondisi tertentu—misalnya buffer tidak kosong. Tanpa condition variable, thread konsumen harus melakukan busy-waiting (loop cek status sambil memegang CPU), yang boros. Condition variable (pthread_cond_t) memungkinkan thread tidur hingga thread lain mengirim sinyal bahwa kondisi sudah terpenuhi. Pada chapter ini kita membangun dua demo: pola wait/signal sederhana antara dua thread, lalu pola klasik producer-consumer dengan buffer berbatas.

Pola Dasar wait/signal #

API condition variable terdiri dari tiga fungsi inti: pthread_cond_wait (menidurkan thread dan melepaskan mutex atomik), pthread_cond_signal (membangunkan satu thread yang menunggu), dan pthread_cond_broadcast (membangunkan semua thread yang menunggu).

c
/* File: cv_dasar.c - pola wait/signal sederhana */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>

static int siap = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  cv  = PTHREAD_COND_INITIALIZER;

static void *pekerja(void *arg) {
    (void) arg;
    printf("pekerja: mulai menyiapkan data...\n");

    /* simulasi pekerjaan 1 detik */
    struct timespec ts = { 1, 0 };
    nanosleep(&ts, NULL);

    pthread_mutex_lock(&mtx);
    siap = 1;
    printf("pekerja: data siap, kirim sinyal\n");
    pthread_cond_signal(&cv);
    pthread_mutex_unlock(&mtx);
    return NULL;
}

int main(void) {
    pthread_t t;
    pthread_create(&t, NULL, pekerja, NULL);

    pthread_mutex_lock(&mtx);
    printf("main: menunggu data siap...\n");
    while (!siap) {
        pthread_cond_wait(&cv, &mtx);
    }
    printf("main: data siap, lanjut eksekusi\n");
    pthread_mutex_unlock(&mtx);

    pthread_join(t, NULL);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -pthread -o cv_dasar cv_dasar.c && ./cv_dasar

Output yang Dihasilkan:

text
main: menunggu data siap...
pekerja: mulai menyiapkan data...
pekerja: data siap, kirim sinyal
main: data siap, lanjut eksekusi

Mari kita telaah baris-baris kode di atas:

  1. Baris 6-8 (state global): Tiga objek global: flag siap, mutex mtx, condition variable cv. Condition variable selalu dipakai bersama mutex—tidak pernah berdiri sendiri.
  2. Baris 19 (pthread_mutex_lock(&mtx);): Pekerja mengunci mutex sebelum menulis siap = 1 dan memberi sinyal. Tanpa kunci ini, terdapat race: main bisa cek siap sebelum pekerja menulisnya, lalu memanggil wait setelah sinyal sudah lewat—main akan menunggu selamanya.
  3. Baris 22 (pthread_cond_signal(&cv);): Membangunkan satu thread yang sedang pthread_cond_wait. Bila tidak ada thread yang menunggu, sinyal hilang (bukan antrian). Inilah mengapa kita selalu cek kondisi di loop.
  4. Baris 30 (while (!siap)): Penting: gunakan while, bukan if. Spurious wakeup (thread dibangunkan tanpa sinyal) diperbolehkan oleh POSIX. Loop while memastikan thread mengecek ulang kondisi setelah bangun.
  5. Baris 31 (pthread_cond_wait(&cv, &mtx);): Operasi atomik: (a) melepaskan mtx agar thread lain bisa masuk critical section, (b) menidurkan thread ini, (c) saat dibangunkan, mengunci kembali mtx sebelum kembali. Tanpa melepas mutex, pekerja tidak akan pernah bisa mengunci untuk mengirim sinyal.

Pola Producer-Consumer dengan Buffer Berbatas #

Pola producer-consumer adalah salah satu penggunaan condition variable paling klasik. Produsen memproduksi item dan menaruh di buffer. Konsumen mengambil item dari buffer. Bila buffer penuh, produsen menunggu; bila kosong, konsumen menunggu. Dua condition variable dipakai: satu untuk “buffer tidak penuh” (sinyal ke produsen), satu untuk “buffer tidak kosong” (sinyal ke konsumen).

c
/* File: cv_prodcon.c - producer-consumer dengan buffer berbatas */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define KAPASITAS 5

static int buffer[KAPASITAS];
static int jumlah = 0;                              /* jumlah item di buffer */
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  tidak_kosong = PTHREAD_COND_INITIALIZER;
static pthread_cond_t  tidak_penuh  = PTHREAD_COND_INITIALIZER;

static void jeda_ms(int ms) {
    struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
    nanosleep(&ts, NULL);
}

static void *produsen(void *arg) {
    long id = (long) arg;
    for (int i = 1; i <= 8; i++) {
        pthread_mutex_lock(&mtx);
        while (jumlah == KAPASITAS) {
            printf("[P%ld] buffer penuh, menunggu...\n", id);
            pthread_cond_wait(&tidak_penuh, &mtx);
        }
        buffer[jumlah++] = i;
        printf("[P%ld] produksi %d (jumlah=%d)\n", id, i, jumlah);
        pthread_cond_signal(&tidak_kosong);
        pthread_mutex_unlock(&mtx);
        jeda_ms(100);
    }
    return NULL;
}

static void *konsumen(void *arg) {
    long id = (long) arg;
    for (int i = 0; i < 8; i++) {
        pthread_mutex_lock(&mtx);
        while (jumlah == 0) {
            printf("[K%ld] buffer kosong, menunggu...\n", id);
            pthread_cond_wait(&tidak_kosong, &mtx);
        }
        int item = buffer[--jumlah];
        printf("[K%ld] konsumsi %d (jumlah=%d)\n", id, item, jumlah);
        pthread_cond_signal(&tidak_penuh);
        pthread_mutex_unlock(&mtx);
        jeda_ms(200);   /* konsumen lebih lambat */
    }
    return NULL;
}

int main(void) {
    pthread_t p, k;
    pthread_create(&p, NULL, produsen, (void *) 1L);
    pthread_create(&k, NULL, konsumen, (void *) 1L);
    pthread_join(p, NULL);
    pthread_join(k, NULL);
    printf("Selesai. jumlah akhir = %d\n", jumlah);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -pthread -o cv_prodcon cv_prodcon.c && ./cv_prodcon

Output yang Dihasilkan (urutan dapat sedikit berbeda antar eksekusi):

text
[K1] buffer kosong, menunggu...
[P1] produksi 1 (jumlah=1)
[K1] konsumsi 1 (jumlah=0)
[P1] produksi 2 (jumlah=1)
[K1] konsumsi 2 (jumlah=0)
[P1] produksi 3 (jumlah=1)
[P1] produksi 4 (jumlah=2)
[K1] konsumsi 4 (jumlah=1)
[P1] produksi 5 (jumlah=2)
[P1] produksi 6 (jumlah=3)
[K1] konsumsi 6 (jumlah=2)
[P1] produksi 7 (jumlah=3)
[P1] produksi 8 (jumlah=4)
[K1] konsumsi 8 (jumlah=3)
[K1] konsumsi 7 (jumlah=2)
[K1] konsumsi 5 (jumlah=1)
[K1] konsumsi 3 (jumlah=0)
Selesai. jumlah akhir = 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 11-13 (dua condition variable): tidak_kosong ditunggu konsumen saat jumlah == 0, diberi sinyal produsen setelah menambah item. tidak_penuh ditunggu produsen saat jumlah == KAPASITAS, diberi sinyal konsumen setelah mengambil item. Dua variabel ini menggambarkan dua kondisi berbeda—tidak bisa digabung menjadi satu.
  2. Baris 20-22 (while (jumlah == KAPASITAS)): Produsen menunggu selama buffer penuh. pthread_cond_wait(&tidak_penuh, &mtx) melepaskan mutex dan menidurkan thread. Saat dibangunkan, mutex dikunci kembali dan loop while dicek ulang.
  3. Baris 26 (pthread_cond_signal(&tidak_kosong);): Setelah menambah item, produsen memberi sinyal “ada item baru” ke konsumen yang mungkin sedang menunggu. Bila tidak ada konsumen yang menunggu, sinyal ini hilang—tetapi tidak masalah karena konsumen berikutnya akan langsung melihat jumlah > 0 di loop while.
  4. Baris 33-35 (while (jumlah == 0)): Konsumen menunggu selama buffer kosong. Logika simetris dengan produsen—wait, cek ulang jumlah, ambil item.
  5. Baris 39 (jeda_ms(200) di konsumen vs jeda_ms(100) di produsen): Konsumen sengaja dibuat lebih lambat agar buffer sempat penuh. Dalam output terlihat produsen sempat menumpuk 3-4 item sebelum konsumen mengambil.

Jebakan Umum #

Tiga kesalahan paling sering ditemui saat memakai condition variable:

  • Tidak memakai mutex: Condition variable tidak menyimpan state—ia hanya sinyal “ada yang berubah”. Anda wajib punya variabel kondisi (siap, jumlah, dll.) yang dilindungi mutex. Tanpa mutex, race condition pada variabel kondisi membuat sinyal bisa hilang.
  • Memakai if alih-alih while: Spurious wakeup dapat membangunkan thread tanpa alasan. Loop while mengecek ulang kondisi dan kembali tidur bila belum terpenuhi.
  • Lupa pthread_cond_signal di akhir critical section: Bila sinyal diberikan sebelum mutex dilepas, beberapa platform mungkin membangunkan thread yang segera mencoba lock mutex (yang masih dipegang pemanggil)—mubazir. Letakkan signal di akhir critical section sebelum unlock, atau gunakan pola signal di luar lock untuk efisiensi pada kasus tertentu.