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.
/* 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:
gcc -std=c11 -Wall -Wextra -pthread -o mutex_counter mutex_counter.c
./mutex_counter && ./mutex_counter && ./mutex_counterOutput yang Dihasilkan:
Ekspektasi: 3000000
Aktual : 3000000
Ekspektasi: 3000000
Aktual : 3000000
Ekspektasi: 3000000
Aktual : 3000000Mari kita telaah baris-baris kode di atas:
- Baris 8 (
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;): Deklarasi mutex global dengan inisialisasi statis. MacroPTHREAD_MUTEX_INITIALIZERmengisi strukturpthread_mutex_tdengan nilai konstanta sehingga tidak perlu memanggilpthread_mutex_initdi runtime. Cocok untuk mutex yang berumur sepanjang program. - 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. - 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. - Baris 14 (
pthread_mutex_unlock(&mtx);): Melepaskan kunci. Salah satu thread yang sedang menunggu akan dibangunkan oleh kernel dan diberi kesempatan mengambil alih kunci. - Baris 23-26 (
pthread_join x3): Setelah ketiga thread selesai,counterdijamin berisi3000000—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.
/* 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:
gcc -std=c11 -Wall -Wextra -pthread -o mutex_trylock mutex_trylock.c && ./mutex_trylockOutput yang Dihasilkan (urutan bisa berbeda antar eksekusi):
[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 4Pada 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.
/* 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:
gcc -std=c11 -Wall -Wextra -pthread -o mutex_dinamis mutex_dinamis.c && ./mutex_dinamisOutput yang Dihasilkan:
Saldo akhir (harus 20000): 20000Mari kita telaah baris-baris kode di atas:
- Baris 8-12 (
Rekeningstruct): Mutex menjadi field dalam struct. Pola ini umum dipakai pada kode berorientasi objek—setiap instanceRekeningpunya kunci sendiri, sehingga dua rekening berbeda dapat diakses paralel tanpa saling mengunci. - Baris 14-17 (
rekening_init): Selain mengisisaldo, fungsi memanggilpthread_mutex_init(&r->mtx, NULL). Argumen keduaNULLmemakai atribut default. Untuk kebutuhan khusus (misalnya mutex bertipePTHREAD_MUTEX_ERRORCHECKyang mendeteksi double-lock), kita buatpthread_mutexattr_tterpisah. - Baris 19-21 (
rekening_destroy): Memanggilpthread_mutex_destroy. Pada Linux, fungsi ini membersihkan struktur internal—wajib dipanggil agar tidak terjadi kebocoran resource, terutama untuk mutex yang atributnya bukan default. - 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. - Baris 37 (
rekening_destroy(&r)): SebelumRekeningkeluar 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.