Deadlock adalah kondisi ketika dua thread (atau lebih) masing-masing memegang satu kunci dan menunggu kunci milik pihak lain — tidak ada yang bisa maju, tidak ada yang bisa mundur, dan program menjadi beku tanpa pesan error apa pun. Berbeda dari crash, deadlock tidak meninggalkan jejak; programnya “hidup” tapi tidak berbuat apa-apa lagi. Chapter ini merangkum empat kondisi yang harus terpenuhi agar deadlock terjadi, menampilkan pola berbahaya yang memicunya (tanpa dijalankan), lalu membangun versi aman yang benar-benar teruji dengan std::lock dan std::scoped_lock.
Empat Kondisi Deadlock #
Literature sistem operasi merangkum syarat deadlock (kondisi Coffman) menjadi empat, dan keempatnya harus terpenuhi sekaligus:
- Mutual exclusion: ada resource yang hanya boleh dipakai satu thread — misalnya mutex.
- Hold and wait: thread memegang satu kunci sambil menunggu kunci lain.
- No preemption: kunci tidak bisa direbut paksa dari pemegangnya; hanya pemegang yang boleh melepas.
- Circular wait: membentuk lingkaran tunggu — A menunggu B, B menunggu A.
Kita tidak bisa menghapus kondisi pertama dan ketiga (memang sifat mutex), jadi strategi pencegahan menyerang dua sisanya: jangan pernah hold-and-wait tanpa jaminan, dan pastikan urutan pengambilan kunci konsisten sehingga circular wait mustahil terbentuk.
Pola yang Memicu Deadlock #
Bentuk klasiknya: dua thread mengunci dua mutex yang sama dengan urutan berbeda.
std::mutex rekeningA;
std::mutex rekeningB;
void thread1() {
std::lock_guard<std::mutex> a(rekeningA); // pegang A...
std::lock_guard<std::mutex> b(rekeningB); // ...lalu tunggu B
}
void thread2() {
std::lock_guard<std::mutex> b(rekeningB); // pegang B...
std::lock_guard<std::mutex> a(rekeningA); // ...lalu tunggu A
}Program di atas sengaja TIDAK dijalankan dan tidak ada keluaran yang diklaim. Jalannya bisa seperti ini: thread1 merebut A, thread2 merebut B; kini thread1 menunggu B yang dipegang thread2, dan thread2 menunggu A yang dipegang thread1. Kedua lock_guard itu tidak pernah dibuat sempurna, kedua destructor tidak pernah jalan, dan kedua thread menunggu selamanya. Menariknya, deadlock ini sering tidak muncul di setiap eksekusi — bila thread1 selesai sebelum thread2 sempat mulai, program berjalan normal. Karena itu deadlock termasuk bug yang paling licin: tersembunyi berbulan-bulan, lalu muncul di produksi.
Pencegahan: Kunci Bersama Sekaligus #
Pustaka standar memberi dua alat yang memecahkan pola di atas. std::lock(m1, m2) mengunci beberapa mutex sekaligus dengan algoritma yang menghindari deadlock (mencoba kunci satu per satu, dan bila ada yang gagal, melepas yang sudah dipegang lalu mengulang). std::scoped_lock (C++17) melakukannya sekaligus dalam bentuk RAII — pilihan paling ringkas untuk beberapa mutex.
Program teruji berikut memakai keduanya untuk operasi transfer antar dua rekening — contoh dunia nyata yang klasik, karena memindahkan uang memang harus mengunci dua saldo bersamaan:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex rekeningA;
std::mutex rekeningB;
int saldoA = 500;
int saldoB = 300;
// Teknik 1: std::scoped_lock mengunci beberapa mutex sekaligus,
// dengan algoritma anti-deadlock di dalamnya (C++17)
void transferAB() {
std::scoped_lock kunci(rekeningA, rekeningB);
saldoA -= 100;
saldoB += 100;
}
// Teknik 2: std::lock mengunci kedua mutex, lalu lock_guard
// "mengadopsi" kunci yang sudah terpasang (adopt_lock)
void transferBA() {
std::lock(rekeningA, rekeningB);
std::lock_guard<std::mutex> kunciA(rekeningA, std::adopt_lock);
std::lock_guard<std::mutex> kunciB(rekeningB, std::adopt_lock);
saldoB -= 50;
saldoA += 50;
}
int main() {
std::thread t1(transferAB);
std::thread t2(transferBA);
t1.join();
t2.join();
std::cout << "saldoA: " << saldoA << "\n";
std::cout << "saldoB: " << saldoB << "\n";
std::cout << "total : " << saldoA + saldoB << "\n";
return 0;
}Output yang Dihasilkan:
saldoA: 450
saldoB: 350
total : 800Bedah barisnya:
- Baris 13 (
std::scoped_lock kunci(rekeningA, rekeningB)): kunci A dan B diperoleh dalam satu langkah yang aman terhadap deadlock — di dalamnya berlaku algoritma yang sama denganstd::lock. Tidak ada jendela “memegang satu, menunggu satu”. - Baris 21 (
std::lock(rekeningA, rekeningB)): teknik klasik — kedua mutex terkunci ketika fungsi ini kembali, urutannya tidak penting bagi keamanan. - Baris 22-23 (
std::adopt_lock):lock_guarddibuat tanpa mengunci ulang; ia mengadopsi kunci yang sudah terpasang olehstd::lock, dan bertugas melepasnya saat scope berakhir. Tanpaadopt_lock,lock_guardakan mencoba mengunci mutex yang sudah kita pegang sendiri — perilaku tak terdefinisi. - Baris 32-33 (
join()): kedua transfer selesai, tidak ada saling tunggu. Saldo akhirnya 450 dan 350; total 800 tetap terjaga karena tiap transfer mengunci kedua rekening sekaligus.
Kebiasaan yang Mencegah Deadlock #
Kedua alat di atas paling berguna bila didukung kebiasaan desain:
- Urutan kunci yang konsisten. Bila semua kode mengunci A sebelum B (tidak pernah sebaliknya), circular wait mustahil terbentuk. Aturan ini berlaku juga untuk hierarki besar: kunci level tinggi selalu sebelum level rendah.
scoped_lockuntuk lebih dari satu mutex. Ia menggantikan pasanganstd::lock+adopt_lockdengan satu baris, dan keliru pakai hampir mustahil.- Minimalkan jumlah kunci yang dipegang bersamaan. Bila bisa, siapkan semua nilai sebelum mengunci, lalu di dalam critical section hanya melakukan perubahan.
- Waspadai kunci tersembunyi. Kode pustaka atau callback yang dipanggil di dalam critical section bisa mengunci mutex lain tanpa kita sadari; jalur deadlock sering bersembunyi di sana.
Mengunci dengan aman menyelesaikan persaingan data, tetapi sering kali thread juga perlu saling memberi tahu: “datamu sudah ada”. Menunggu sinyal tanpa membakar CPU adalah pekerjaan condition_variable — topik chapter berikutnya.
Referensi: cppreference — std::lock , cppreference — std::scoped_lock .