Dua thread yang menulis ke variabel yang sama tanpa aturan adalah resep hasil yang salah — dan yang lebih berbahaya, hasil yang salah secara diam-diam, kadang benar kadang tidak, tergantung keberuntungan penjadwalan. std::mutex adalah alat pertahanan paling dasar untuk situasi itu: objek kunci yang hanya boleh dipegang satu thread pada satu waktu. Thread yang datang kedua menunggu sampai kunci dilepas. Chapter ini menunjukkan pola rusak yang harus dihindari, lalu membangun versi aman yang hasil akhirnya deterministik dan bisa diuji.

Data Race Tanpa Mutex #

Perhatikan potongan program berikut. Kedua thread menambah gaji sebanyak 100.000 kali; nilai akhir yang benar seharusnya 20.000.000.

cpp Versi tanpa mutex — mengandung data race, JANGAN dijalankan
#include <thread>

long long gaji = 0;

void tambahGaji(int jumlahTransaksi) {
    for (int i = 0; i < jumlahTransaksi; ++i) {
        gaji += 100;   // baca -> tambah -> tulis, tanpa sinkronisasi
    }
}

int main() {
    std::thread kasirA(tambahGaji, 100000);
    std::thread kasirB(tambahGaji, 100000);
    kasirA.join();
    kasirB.join();
}

Blok di atas sengaja tidak dijalankan dan tidak ada keluaran yang kita klaim. Ada dua alasan. Pertama, operasi gaji += 100 bukan satu langkah: CPU membaca nilai, menambahnya, lalu menulis kembali. Bila thread B membaca gaji di antara baca dan tulis milik thread A, tambahan dari salah satu kasir hilang begitu saja (lost update) — hasil akhir biasanya di bawah 20.000.000 dan berubah-ubah tiap eksekusi. Kedua, dan ini penting secara standar: kejadian seperti itu adalah data race, dan standar C++ menyatakan program dengan data race berperilaku undefined behavior — semua taruhan dibatalkan, bukan sekadar “hasilnya agak melenceng”. Chapter data race membedah definisinya secara formal.

Melindungi Critical Section dengan std::mutex #

Solusinya: bagian kode yang menyentuh data bersama dibungkus kunci. Bagian itu disebut critical section — wilayah yang hanya boleh dihuni satu thread pada satu waktu. Program berikut adalah versi aman dari contoh di atas, dan hasil akhirnya selalu 20.000.000:

cpp
#include <iostream>
#include <mutex>
#include <thread>

std::mutex gajiMutex;   // melindungi variabel gaji
long long gaji = 0;

void tambahGaji(int jumlahTransaksi) {
    for (int i = 0; i < jumlahTransaksi; ++i) {
        // critical section: hanya satu thread boleh mengubah gaji
        std::lock_guard<std::mutex> kunci(gajiMutex);
        gaji += 100;
    }   // kunci lepas otomatis di sini
}

int main() {
    std::thread kasirA(tambahGaji, 100000);
    std::thread kasirB(tambahGaji, 100000);

    kasirA.join();
    kasirB.join();

    std::cout << "gaji akhir: " << gaji << "\n";
    return 0;
}

Output yang Dihasilkan:

text
gaji akhir: 20000000

Bedah barisnya:

  1. Baris 5-6 (std::mutex gajiMutex; long long gaji = 0;): kunci dan data yang dilindungi dideklarasikan berdampingan. Pasangan yang rapat seperti ini membuat pembaca kode langsung melihat data mana yang dilindungi kunci mana.
  2. Baris 11 (std::lock_guard ... kunci(gajiMutex)): kunci diambil. Bila thread lain sedang memegang gajiMutex, pemanggil menunggu (state Blocked) sampai kunci lepas. Di dalam blok ini hanya satu thread yang aktif menyentuh gaji.
  3. Baris 12 (gaji += 100;): baca-tambah-tulis kini terjadi di bawah perlindungan kunci, sehingga tidak bisa terseling dengan thread lain.
  4. Baris 13 (}): lock_guard dilepas di akhir tiap iterasi. Mereleaskan kunci tiap putaran (bukan memegangnya sepanjang loop) memberi thread lain kesempatan masuk dan menjaga critical section sekecil mungkin.
  5. Baris 20-21 (join()): main() hanya mencetak setelah kedua kasir selesai — hasil akhirnya deterministik: 200.000 transaksi kali 100.

Catatan jujur tentang mutex: ia menjamin kebenaran, bukan kecepatan. Ketika thread B menunggu kunci, ia tidak mengerjakan apa pun. Pada program di atas biaya itu kecil; pada program yang critical section-nya panjang, antrean kunci bisa menjadi bottleneck. Prinsipnya sederhana — jaga critical section tetap pendek.

Kebiasaan yang Menjaga Mutex Tetap Benar #

Mutex hanya efektif bila disiplin dipakai. Tiga kebiasaan ini mencegah mayoritas insiden:

  • Setiap akses harus lewat kunci yang sama. Satu akses lalai — bahkan hanya satu baris “cuma baca” — cukup untuk membatalkan jaminan semua akses lain.
  • Jangan memanggil kode asing sambil memegang kunci. Fungsi yang kita panggil bisa saja mengambil kunci lain atau kunci yang sama lagi, dan di situ masalah terselubung dimulai.
  • Kunci dan data harus satu pasangan yang jelas. Satu std::mutex untuk satu kelompok data; dua data berbeda tidak harus satu kunci, karena kunci yang terlalu besar membatasi paralelisme tanpa alasan.

Kita sudah melihat lock_guard bekerja tanpa dijelaskan tuntas. Chapter berikutnya membongkarnya: keluarga wrapper RAII untuk kunci, dari lock_guard yang kaku sampai unique_lock yang fleksibel.

Referensi: cppreference — std::mutex , cppreference — std::lock_guard .