Mengunci mutex secara manual dengan lock() dan unlock() membuat kita bertanggung jawab atas setiap jalur keluar dari critical section — satu return dini atau satu exception yang lolos, dan kunci terjebak selamanya. Pustaka standar menjawab dengan pola RAII yang sudah kita kenal dari smart pointer: pasangkan umur kunci dengan umur objek pembungkus. Ada tiga pembungkus yang perlu kita kuasai: std::lock_guard yang sederhana dan kaku, std::unique_lock yang fleksibel, dan std::shared_lock yang bekerja dengan std::shared_mutex untuk pola banyak-pembaca. Program teruji di chapter ini mencoba kelimanya sekaligus — dan seluruh keluarannya deterministik.
lock_guard: Kunci untuk Satu Scope #
std::lock_guard melakukan dua hal dan hanya dua hal: mengunci mutex di constructor, melepasnya di destructor. Karena itu bentuk pemakaiannya selalu sama — deklarasikan di baris pertama critical section, dan biarkan akhir scope yang bekerja. Tidak ada cara untuk melepas kunci lebih awal, tidak ada API tambahan; itulah harga dan keunggulannya. Untuk critical section biasa, ia tetap menjadi pilihan default karena minimalis: tidak ada kemungkinan salah pakai yang tidak terlihat.
unique_lock: Fleksibel Saat Dibutuhkan #
std::unique_lock menawarkan superset kemampuan lock_guard dengan biaya kecil (flag internal yang ikut terbawa). Kemampuan yang sering dipakai:
std::defer_lock: objek dibuat tanpa mengunci; kita mengunci belakangan denganlock()— berguna saat ada pekerjaan lain yang perlu dilakukan sebelum critical section, atau saat kunci akan dilewatkan kestd::lock.owns_lock(): menanyakan apakah kunci sedang dipegang.unlock()manual: critical section bisa ditutup lebih awal tanpa menunggu akhir scope.- Batas waktu: pada
std::timed_mutex,try_lock_for()/try_lock_until()mencoba mengunci dalam durasi tertentu dan melapor gagal alih-alih menunggu selamanya. - Bisa dipindah:
unique_lockmovable — kepemilikan kunci bisa diserahkan antar objek, misalnya dikembalikan dari fungsi.
shared_mutex: Banyak Pembaca, Satu Penulis #
Sejak C++17 ada std::shared_mutex dengan dua mode kunci. Mode eksklusif (lewat std::unique_lock atau lock()) berperilaku seperti mutex biasa: hanya satu thread. Mode bersama (lewat std::shared_lock) boleh dipegang banyak thread sekaligus, selama tidak ada penulis. Pola ini menguntungkan saat data lebih sering dibaca daripada ditulis — cache, tabel konfigurasi, peta data hampir-hampir statis — karena pembaca-pembaca itu tidak lagi saling mengantri.
#include <chrono>
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>
using namespace std::chrono_literals;
int main() {
std::cout << std::boolalpha;
// 1) lock_guard: kunci dipasang di constructor, dilepas di akhir scope
{
std::mutex m;
int hitung = 0;
std::thread t1([&] {
std::lock_guard<std::mutex> kunci(m);
++hitung;
});
std::thread t2([&] {
std::lock_guard<std::mutex> kunci(m);
++hitung;
});
t1.join();
t2.join();
std::cout << "lock_guard, hitung : " << hitung << "\n";
}
// 2) unique_lock dengan defer_lock: mengunci belakangan, fleksibel
{
std::mutex m;
int stok = 10;
std::unique_lock<std::mutex> kunci(m, std::defer_lock);
std::cout << "sebelum lock owns : " << kunci.owns_lock() << "\n";
kunci.lock(); // baru mengunci di sini
std::cout << "setelah lock owns : " << kunci.owns_lock() << "\n";
stok -= 3;
kunci.unlock(); // bisa dilepas sebelum scope berakhir
std::cout << "unique_lock, stok : " << stok << "\n";
}
// 3) unique_lock pada timed_mutex: menunggu kunci dengan batas waktu
{
std::timed_mutex m;
std::thread penuhi([&] {
std::lock_guard<std::timed_mutex> kunci(m);
std::this_thread::sleep_for(200ms); // memegang kunci sejenak
});
std::this_thread::sleep_for(50ms);
std::unique_lock<std::timed_mutex> kunci(m, std::defer_lock);
bool dapat = kunci.try_lock_for(2s); // batas longgar: pasti dapat
std::cout << "try_lock_for 2s : " << dapat << "\n";
penuhi.join();
}
// 4) shared_mutex: banyak pembaca bersamaan, penulis eksklusif
{
std::shared_mutex m;
int data = 0;
std::thread penulis([&] {
std::unique_lock<std::shared_mutex> kunci(m);
data = 42;
});
penulis.join(); // tulis selesai dulu supaya hasil baca pasti
int baca1 = 0;
int baca2 = 0;
std::thread pembaca1([&] {
std::shared_lock<std::shared_mutex> kunci(m);
baca1 = data;
});
std::thread pembaca2([&] {
std::shared_lock<std::shared_mutex> kunci(m);
baca2 = data;
});
pembaca1.join();
pembaca2.join();
std::cout << "shared_lock, baca : " << baca1 << ", " << baca2 << "\n";
}
return 0;
}Output yang Dihasilkan:
lock_guard, hitung : 2
sebelum lock owns : false
setelah lock owns : true
unique_lock, stok : 7
try_lock_for 2s : true
shared_lock, baca : 42, 42Bedah tiap bagian:
- Bagian 1 (baris 15-22): dua thread masing-masing menaikkan
hitungdi bawahlock_guardyang sama. Tanpa kunci,++hitungadalah data race; dengan kunci, hasilnya tepat 2. - Bagian 2 (baris 32-38):
unique_lockdibuat denganstd::defer_lock, sehinggaowns_lock()masihfalse— kunci belum dipegang. Setelahkunci.lock()bernilaitrue, perubahanstokdilindungi, dankunci.unlock()melepasnya sebelum scope berakhir. - Bagian 3 (baris 44-52): thread
penuhimemegangtimed_mutexselama 200 milidetik.try_lock_for(2s)menunggu sampai dua detik — batas yang jauh lebih longgar dari waktu pegangan, sehingga hasilnya selalutruedan program ini deterministik. Bila batasnya dipendekkan, hasilnya menjadi bergantung waktu — bentuk nondeterminisme yang kita hindari pada contoh teruji. - Bagian 4 (baris 60-77): penulis menulis
data = 42dengan kunci eksklusif, lalu di-join()dulu supaya nilai yang dibaca pasti. Dua pembaca kemudian mengunci denganshared_lock— keduanya boleh memegang kunci bersama secara serentak, dan keduanya membaca 42.
Semua wrapper di atas mengunci satu mutex pada satu waktu. Masalah baru muncul ketika satu operasi butuh dua kunci sekaligus — di situ urutan pengambilan kunci bisa memperangkap dua thread saling menunggu. Fenomena itu bernama deadlock, dan itulah topik chapter berikutnya.
call_once dan once_flag: Inisiasi Tepat Sekali #
Banyak kebutuhan sinkronisasi bukan soal “siapa masuk duluan”, melainkan “pastikan ini hanya terjadi sekali”: memuat konfigurasi, membuka koneksi, atau mengisi cache global. Menambahkan bool sudahInit yang diperiksa tiap thread butuh mutex sendiri dan rawan salah pola (race pada pembacaan flag). C++ memberi pasangan khusus untuknya: std::call_once(fungsi, args...) bersama std::once_flag. Berapa pun thread yang memanggil, fungsi hanya dieksekusi tepat satu kali; thread lain menunggu sampai eksekusi selesai, lalu kembali tanpa memanggilnya.
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
namespace {
std::once_flag penanda;
int jumlahInisiasi = 0;
void inisiasiMahal() {
++jumlahInisiasi; // membuktikan fungsi hanya dijalankan sekali
}
} // namespace
int main() {
std::vector<std::thread> tim;
std::vector<int> bekerjaSebelum{0, 0, 0, 0}; // slot milik tiap thread
for (int i = 0; i < 4; ++i) {
tim.emplace_back([&bekerjaSebelum, i] {
std::call_once(penanda, inisiasiMahal);
++bekerjaSebelum[i]; // setiap thread menandai slot sendiri
});
}
for (std::thread& t : tim) {
t.join();
}
std::cout << "inisiasi mahal berjalan " << jumlahInisiasi
<< " kali (harus 1)\n";
std::cout << "thread yang selesai: " << bekerjaSebelum[0]
<< bekerjaSebelum[1] << bekerjaSebelum[2] << bekerjaSebelum[3]
<< " dari 4\n";
return 0;
}Output yang Dihasilkan:
inisiasi mahal berjalan 1 kali (harus 1)
thread yang selesai: 1111 dari 4Dua hal membuat program ini deterministik. Setiap thread hanya menulis ke slot bekerjaSebelum miliknya sendiri — pola “slot tetap per thread” yang kita pegang sejak chapter std::thread — dan ringkasan dicetak setelah seluruh thread di-join. once_flag sendiri tidak bisa disalin maupun dipindah: ia milik satu titik inisiasi, bukan nilai yang dipindah-pindahkan.
Referensi: cppreference — std::unique_lock , cppreference — std::shared_mutex , cppreference — std::shared_lock .