Mutex (mutual exclusion) adalah kunci yang memastikan hanya satu thread pada satu waktu yang boleh memasuki suatu critical section—blok kode yang mengakses shared mutable state. Saat thread A memegang mutex, thread B yang ingin masuk critical section yang sama akan memblokir hingga A melepaskannya. Pada chapter ini kita memperbaiki program race condition dari chapter sebelumnya: tiga thread meng-increment counter satu juta kali masing-masing, dengan hasil akhir yang konsisten tiga juta. Setelah itu, kita melihat pola trylock (non-blocking) dan inisialisasi dinamis dengan pthread_mutex_init/pthread_mutex_destroy.

Memperbaiki Race Condition dengan Mutex #

Kita mulai dari program counter ber-race dari chapter sebelumnya, lalu menambahkan dua baris kunci: pthread_mutex_lock sebelum counter++ dan pthread_mutex_unlock setelahnya. Mutex dideklarasikan sebagai variabel global dengan macro inisialisasi statis PTHREAD_MUTEX_INITIALIZER.

c
/* File: mutex_counter.c - perbaiki race condition dengan mutex */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>

#define N_ITER 1000000
static long counter = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void *naikkan(void *arg) {
    (void) arg;
    for (long i = 0; i < N_ITER; i++) {
        pthread_mutex_lock(&mtx);     /* masuk critical section */
        counter++;
        pthread_mutex_unlock(&mtx);   /* keluar critical section */
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, naikkan, NULL);
    pthread_create(&t2, NULL, naikkan, NULL);
    pthread_create(&t3, NULL, naikkan, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);

    printf("Ekspektasi: %d\n", 3 * N_ITER);
    printf("Aktual    : %ld\n", counter);
    return 0;
}

Compile dan jalankan beberapa kali:

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

Output yang Dihasilkan:

text
Ekspektasi: 3000000
Aktual    : 3000000
Ekspektasi: 3000000
Aktual    : 3000000
Ekspektasi: 3000000
Aktual    : 3000000

Mari kita telaah baris-baris kode di atas:

  1. Baris 8 (static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;): Deklarasi mutex global dengan inisialisasi statis. Macro PTHREAD_MUTEX_INITIALIZER mengisi struktur pthread_mutex_t dengan nilai konstanta sehingga tidak perlu memanggil pthread_mutex_init di runtime. Cocok untuk mutex yang berumur sepanjang program.
  2. Baris 12 (pthread_mutex_lock(&mtx);): Meminta kunci. Bila mutex belum dipegang, thread ini langsung memegangnya dan melanjutkan. Bila sudah dipegang thread lain, thread ini memblokir (tidur di kernel) hingga pemegang melepaskan.
  3. Baris 13 (counter++;): Sekarang hanya satu thread yang berada di dalam blok ini pada satu waktu. Operasi read-modify-write yang sebelumnya race menjadi aman.
  4. Baris 14 (pthread_mutex_unlock(&mtx);): Melepaskan kunci. Salah satu thread yang sedang menunggu akan dibangunkan oleh kernel dan diberi kesempatan mengambil alih kunci.
  5. Baris 23-26 (pthread_join x3): Setelah ketiga thread selesai, counter dijamin berisi 3000000—tidak ada lagi update yang hilang.

Pola trylock: Non-Blocking Alternative #

Kadang kita tidak ingin memblokir—misalnya thread pekerja yang punya pekerjaan lain. Fungsi pthread_mutex_trylock mengembalikan 0 bila berhasil mendapat kunci, atau kode error EBUSY (16) bila kunci sedang dipegang. Thread dapat memutuskan: mencoba lagi nanti atau lewat.

c
/* File: mutex_trylock.c - pola trylock non-blocking */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#include <time.h>

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void *pekerja(void *arg) {
    long id = (long) arg;
    for (int i = 0; i < 5; i++) {
        int rc = pthread_mutex_trylock(&mtx);
        if (rc == 0) {
            printf("[T%ld] dapat kunci, iterasi %d\n", id, i);
            pthread_mutex_unlock(&mtx);
        } else {
            printf("[T%ld] kunci sibuk, iterasi %d (rc=%d)\n", id, i, rc);
        }
        struct timespec ts = { 0, 1000000 };   /* 1 ms */
        nanosleep(&ts, NULL);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, pekerja, (void *) 1L);
    pthread_create(&t2, NULL, pekerja, (void *) 2L);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan (urutan bisa berbeda antar eksekusi):

text
[T1] kunci sibuk, iterasi 0 (rc=16)
[T2] dapat kunci, iterasi 0
[T1] dapat kunci, iterasi 1
[T2] kunci sibuk, iterasi 1 (rc=16)
[T1] dapat kunci, iterasi 2
[T2] dapat kunci, iterasi 2
[T1] dapat kunci, iterasi 3
[T2] dapat kunci, iterasi 3
[T1] dapat kunci, iterasi 4
[T2] dapat kunci, iterasi 4

Pada iterasi awal, kedua thread hampir bersamaan mencoba trylock. Satu berhasil (rc=0), yang lain gagal (rc=16 = EBUSY). Thread yang gagal tidak memblokir—ia melanjutkan dan mencoba lagi di iterasi berikutnya setelah nanosleep 1 ms.

Mutex Dinamis: init dan destroy #

Inisialisasi statis dengan PTHREAD_MUTEX_INITIALIZER hanya cocok untuk mutex global. Untuk mutex yang merupakan field dalam struct, atau yang atributnya perlu disesuaikan (misalnya tipe error-checking), gunakan pthread_mutex_init saat runtime dan pthread_mutex_destroy saat selesai.

c
/* File: mutex_dinamis.c - mutex sebagai field struct dengan init/destroy */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>

typedef struct {
    long saldo;
    pthread_mutex_t mtx;
} Rekening;

static void rekening_init(Rekening *r, long saldo_awal) {
    r->saldo = saldo_awal;
    pthread_mutex_init(&r->mtx, NULL);   /* alokasi sumber daya kernel */
}

static void rekening_destroy(Rekening *r) {
    pthread_mutex_destroy(&r->mtx);      /* lepaskan sumber daya */
}

static void tambah(Rekening *r, long jumlah) {
    pthread_mutex_lock(&r->mtx);
    r->saldo += jumlah;
    pthread_mutex_unlock(&r->mtx);
}

static void *penabung(void *arg) {
    Rekening *r = (Rekening *) arg;
    for (int i = 0; i < 10000; i++) tambah(r, 1);
    return NULL;
}

int main(void) {
    Rekening r;
    rekening_init(&r, 0);

    pthread_t t1, t2;
    pthread_create(&t1, NULL, penabung, &r);
    pthread_create(&t2, NULL, penabung, &r);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Saldo akhir (harus 20000): %ld\n", r.saldo);
    rekening_destroy(&r);
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
Saldo akhir (harus 20000): 20000

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-12 (Rekening struct): Mutex menjadi field dalam struct. Pola ini umum dipakai pada kode berorientasi objek—setiap instance Rekening punya kunci sendiri, sehingga dua rekening berbeda dapat diakses paralel tanpa saling mengunci.
  2. Baris 14-17 (rekening_init): Selain mengisi saldo, fungsi memanggil pthread_mutex_init(&r->mtx, NULL). Argumen kedua NULL memakai atribut default. Untuk kebutuhan khusus (misalnya mutex bertipe PTHREAD_MUTEX_ERRORCHECK yang mendeteksi double-lock), kita buat pthread_mutexattr_t terpisah.
  3. Baris 19-21 (rekening_destroy): Memanggil pthread_mutex_destroy. Pada Linux, fungsi ini membersihkan struktur internal—wajib dipanggil agar tidak terjadi kebocoran resource, terutama untuk mutex yang atributnya bukan default.
  4. Baris 23-27 (tambah): Operasi kritis dilindungi mutex milik rekening tersebut. Bila ada banyak rekening dan banyak thread, hanya thread yang mengakses rekening sama yang saling menunggu—tidak mengganggu rekening lain.
  5. Baris 37 (rekening_destroy(&r)): Sebelum Rekening keluar dari scope, kita panggil destroy. Bila lupa, pada beberapa platform resource kernel bisa bocor—meskipun pada Linux destructor otomatis ketika process exit.

Kapan Tidak Memakai Mutex #

Mutex memiliki biaya: setiap lock/unlock membutuhkan beberapa mikrodetik untuk atomik operasi dan potensi context switch. Untuk counter sederhana atau flag boolean, atomics (chapter Atomics dalam Praktik) lebih ringan. Untuk skenario producer-consumer di mana thread perlu menunggu kondisi, condition variables (chapter berikutnya) lebih tepat. Tiga alat tersebut—mutex, atomics, condition variable—saling melengkapi dan harus dipilih sesuai pola akses data.