Apa itu mutex?

Mutex, singkatan dari Mutual Exclusion, adalah mekanisme sinkronisasi yang digunakan untuk memastikan bahwa hanya satu proses (thread) yang dapat mengakses suatu resources pada satu waktu. Mutex mencegah terjadinya kondisi balapan (race condition) ketika beberapa thread berusaha mengakses data yang sama secara bersamaan.

Ilustrasi Konsep Mutex

Tanpa Mutex (Race Condition):
┌─────────┐      ┌─────────┐
│ Thread1 │      │ Thread2 │
└────┬────┘      └────┬────┘
     │                │
     ├───► Read: 0   │
     │                ├───► Read: 0
     ├───► +300      │
     │                ├───► +500
     ├───► Write:300 │
     │                └───► Write:500

Result: 500 SALAH (seharusnya 800)


Dengan Mutex (Terlindungi):
┌─────────┐      ┌─────────┐
│ Thread1 │      │ Thread2 │
└────┬────┘      └────┬────┘
     │                │
     ├───► Lock      │
     ├───► Read: 0   │
     ├───► +300      │
     ├───► Write:300 │
     ├───► Unlock    │
     │                ├───► Lock
     │                ├───► Read: 300
     │                ├───► +500
     │                ├───► Write:800
     │                └───► Unlock

Result: 800 BENAR

Kenapa Mutex Diperlukan?

Ketika beberapa thread mengakses dan memodifikasi data yang sama tanpa sinkronisasi, bisa terjadi race condition. Bayangkan dua orang mencoba update saldo rekening bank secara bersamaan:

  1. Thread A baca saldo: 0
  2. Thread B baca saldo: 0 (masih 0 karena Thread A belum selesai update)
  3. Thread A tambah 300, tulis: 300
  4. Thread B tambah 500, tulis: 500 (menimpa hasil Thread A!)
  5. Hasil akhir: 500 (padahal seharusnya 800!)

Mutex memastikan operasi baca-modifikasi-tulis dilakukan secara atomik (tidak terputus).

Implementasi Mutex

Struktur Proyek

.
├── main.c
├── wallet.c
└── wallet.h

Step 1: Deklarasi Interface Wallet

Pertama, kita deklarasikan fungsi wallet yang akan kita gunakan. Library fungsi sederhana untuk memanajemen wallet. Kita bisa melihat saldo dan memperbarui saldo.

#ifndef WALLET_H
#define WALLET_H

int lihat_saldo();
void update_saldo(int nominal);

#endif

Step 2: Implementasi Fungsi Wallet

Selanjutnya, kita implementasikan fungsi wallet dengan delay untuk mensimulasikan operasi I/O atau proses yang membutuhkan waktu. Ini adalah skenario yang sangat rentan terhadap race condition.

#include "wallet.h"
#include <unistd.h>

// Variable global untuk menyimpan saldo
int saldo = 0;

int lihat_saldo()
{
    // Simulasi delay baca database (200ms)
    usleep(200000); // 200 milidetik = 0.2 detik
    return saldo;
}

void update_saldo(int nominal)
{
    // Simulasi delay tulis database (200ms)
    usleep(200000);
    saldo = nominal;
}

Catatan: Fungsi usleep() mensimulasikan operasi yang membutuhkan waktu (seperti akses database atau file I/O). Delay ini membuat race condition lebih mudah terjadi karena memberikan kesempatan thread lain untuk mengakses data di tengah-tengah operasi.

Step 3: Program Utama dengan Mutex

Selanjutnya, kita buat implementasi multithreading dengan mutex untuk melindungi akses ke saldo wallet.

#include "wallet.h"
#include <pthread.h>
#include <stdio.h>

// Deklarasi fungsi thread
void *deposit(void *arg);

// Deklarasi mutex sebagai variable global
pthread_mutex_t mutex;

int main(void)
{
    int saldo_awal = lihat_saldo();
    printf("Saldo awal: %d\n", saldo_awal);

    // Membuat identifier untuk dua thread
    pthread_t thread1, thread2;

    // Inisialisasi mutex dengan atribut default (NULL)
    pthread_mutex_init(&mutex, NULL);

    // Jumlah deposit untuk masing-masing thread
    int deposit1 = 300;
    int deposit2 = 500;

    printf("\nMemulai deposit dari 2 thread...\n");

    // Thread untuk menambah saldo
    pthread_create(&thread1, NULL, deposit, &deposit1);
    pthread_create(&thread2, NULL, deposit, &deposit2);

    // Menunggu kedua thread selesai eksekusi
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Hancurkan mutex setelah selesai digunakan
    pthread_mutex_destroy(&mutex);

    int saldo_akhir = lihat_saldo();
    printf("\nSaldo akhir: %d\n", saldo_akhir);
    printf("Expected: %d\n", saldo_awal + deposit1 + deposit2);

    return 0;
}

void *deposit(void *nominal)
{
    // CRITICAL SECTION START
    // Lock mutex - thread lain harus menunggu
    pthread_mutex_lock(&mutex);

    printf("Thread sedang memproses deposit %d...\n", *(int *)nominal);

    // Baca saldo saat ini
    int saldo = lihat_saldo();

    // Tambahkan nominal deposit
    saldo += *(int *)nominal;

    // Update saldo baru
    update_saldo(saldo);

    printf("Deposit %d berhasil!\n", *(int *)nominal);

    // Unlock mutex - thread lain bisa masuk
    pthread_mutex_unlock(&mutex);
    // CRITICAL SECTION END

    return NULL;
}

Penjelasan Detail

1. Inisialisasi Mutex

pthread_mutex_init(&mutex, NULL);
  • Parameter pertama: pointer ke mutex
  • Parameter kedua: atribut mutex (NULL = default)

2. Lock Mutex

pthread_mutex_lock(&mutex);
  • Thread yang memanggil ini akan mendapat “kunci”
  • Thread lain yang memanggil lock() akan terblokir (menunggu)
  • Memastikan hanya 1 thread yang masuk critical section

3. Critical Section

Bagian code antara lock() dan unlock():

int saldo = lihat_saldo();      // Baca
saldo += *(int *)nominal;       // Modifikasi
update_saldo(saldo);            // Tulis

Ini adalah bagian yang harus dilindungi dari akses konkuren.

4. Unlock Mutex

pthread_mutex_unlock(&mutex);
  • Melepas “kunci” agar thread lain bisa masuk
  • Thread yang sedang menunggu akan mendapat giliran

5. Destroy Mutex

pthread_mutex_destroy(&mutex);
  • Membersihkan resource yang dialokasikan untuk mutex
  • Harus dipanggil setelah semua thread selesai

Visualisasi Eksekusi

Timeline Eksekusi dengan Mutex:

TimeThread1Thread2Saldo
t0startstart0
t1LOCK(waiting…)0
t2read: 0(waiting…)0
t3calc: 0+300(waiting…)0
t4write: 300(waiting…)300
t5UNLOCKLOCK300
t6FINISHread: 300300
t7calc: 300+500300
t8write: 800800
t9UNLOCK800
t10FINISH800

Kompilasi dan Menjalankan

Untuk compile program dengan pthread:

gcc -o wallet main.c wallet.c -lpthread
./wallet

Output yang diharapkan:

Saldo awal: 0

Memulai deposit dari 2 thread...
Thread sedang memproses deposit 300...
Deposit 300 berhasil!
Thread sedang memproses deposit 500...
Deposit 500 berhasil!

Saldo akhir: 800
Expected: 800

Best Practices

  1. Selalu unlock mutex - Gunakan pattern yang konsisten agar tidak lupa unlock
  2. Minimize critical section - Hanya lindungi bagian yang benar-benar perlu
  3. Hindari nested locks - Bisa menyebabkan deadlock1
  4. Destroy mutex - Bebaskan resource setelah selesai
  5. Error handling - Cek return value dari fungsi pthread

Kesimpulan

Mutex adalah tool fundamental untuk sinkronisasi thread. Dengan mutex, kita bisa:

  • Mencegah race condition
  • Memastikan konsistensi data
  • Mengontrol akses ke shared resources

Ingat: Lock → Critical Section → Unlock adalah pattern dasar yang harus selalu diikuti!

Footnotes

  1. Situasi yang terjadi ketika 2 atau lebih proses terjebak saling menunggu untuk mengakses masing-masing resources, sehingga tidak ada satu pun proses yang berjalan.