Gejala Silent Hang pada Worker Queue
Worker queue backend sering kali berhenti memproses antrean pekerjaan tanpa memicu crash atau pesan kesalahan (silent hang). Kondisi ini umumnya terjadi saat proses worker masih berstatus RUNNING atau SLEEPING di tingkat sistem operasi, konsumsi CPU turun menjadi mendekati 0%, namun tidak ada job baru yang selesai dieksekusi.
Penyebab utama skenario ini pada aplikasi berbasis multi-threading (seperti C++, Rust, atau native extensions di runtime lain) adalah lock contention yang berujung pada deadlock. Dua atau lebih thread masuk ke dalam status circular wait saat mencoba mengakuisisi mutex yang sedang ditahan oleh thread lain. Karena panggilan penguncian bersifat memblokir (blocking acquisition), thread tertahan tanpa batas waktu di level kernel syscall (futex).
Reproduksi Masalah: Deadlock Antar-Thread
Berikut adalah contoh minimal kode worker dalam C++17 yang mereproduksi kondisi circular wait. Dua worker thread memproses resource bersama (Resource A dan Resource B) dengan urutan penguncian terbalik.
// worker_deadlock.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <unistd.h>
std::mutex mutex_a;
std::mutex mutex_b;
void worker_job_1() {
std::cout << "[Worker 1] Mengunci mutex_a...\n";
std::unique_lock<std::mutex> lock_a(mutex_a);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulasi jeda I/O
std::cout << "[Worker 1] Menunggu mutex_b...\n";
std::unique_lock<std::mutex> lock_b(mutex_b); // Terblokir di sini
std::cout << "[Worker 1] Selesai memproses job 1\n";
}
void worker_job_2() {
std::cout << "[Worker 2] Mengunci mutex_b...\n";
std::unique_lock<std::mutex> lock_b(mutex_b);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulasi jeda I/O
std::cout << "[Worker 2] Menunggu mutex_a...\n";
std::unique_lock<std::mutex> lock_a(mutex_a); // Terblokir di sini
std::cout << "[Worker 2] Selesai memproses job 2\n";
}
int main() {
std::cout << "Worker process PID: " << getpid() << "\n";
std::thread t1(worker_job_1);
std::thread t2(worker_job_2);
t1.join();
t2.join();
return 0;
}
Kompilasi kode di atas dengan flag debug aktif (-g):
g++ -g -O0 -pthread worker_deadlock.cpp -o worker_deadlock
./worker_deadlock
Program akan berhenti setelah mencetak pesan penguncian pertama dan tidak pernah keluar.
Attach Debugger Menggunakan Grand Unified Debugger (GUD)
Grand Unified Debugger (GUD) merupakan antarmuka bawaan Emacs yang menyediakan visualisasi interaktif untuk debugger baris perintah seperti GDB dan PDB. GUD memetakan posisi eksekusi stack frame debugger langsung ke source code buffer.
- Jalankan Emacs di terminal atau GUI:
emacs. - Buka interface GUD dengan menekan tombol:
M-x gdb. - Saat diminta perintah target di minibuffer, arahkan langsung ke PID proses worker yang macet:
gdb -i=mi -p <PID_WORKER>
Jika menggunakan mode antarmuka standar non-MI:
M-x gud-gdb
Run gud-gdb (like this): gdb --annotate=3 worker_deadlock
(gdb) attach <PID_WORKER>
GDB akan menginterupsi proses target dan menampilkan prompt di buffer Emacs. Source code C++ akan otomatis dimuat di buffer sebelahnya.
Analisis Multi-Thread: Menemukan Circular Wait
Lakukan inspeksi terhadap seluruh thread yang ada di dalam proses worker.
1. Periksa Daftar Thread
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7d92740 (LWP 12450) "worker_deadlock" 0x00007ffff7fa364c in __futex_abstimed_wait_common () from /lib64/libc.so.6
2 Thread 0x7ffff77fe640 (LWP 12451) "worker_deadlock" 0x00007ffff7fa364c in __futex_abstimed_wait_common () from /lib64/libc.so.6
3 Thread 0x7ffff6ffd640 (LWP 12452) "worker_deadlock" 0x00007ffff7fa364c in __futex_abstimed_wait_common () from /lib64/libc.so.6
Thread 2 dan Thread 3 keduanya berada pada syscall __futex_abstimed_wait_common atau __lll_lock_wait.
2. Analisis Backtrace Seluruh Thread
Jalankan perintah thread apply all bt untuk membaca call stack masing-masing thread secara bersamaan:
(gdb) thread apply all bt
Thread 3 (Thread 0x7ffff6ffd640 (LWP 12452)):
#0 0x00007ffff7fa364c in __lll_lock_wait () from /lib64/libc.so.6
#1 0x00007ffff7fa0123 in pthread_mutex_lock () from /lib64/libc.so.6
#2 0x0000000000401341 in std::mutex::lock (this=0x4040a0 <mutex_a>) at /usr/include/c++/v1/mutex:267
#3 0x0000000000401275 in worker_job_2 () at worker_deadlock.cpp:25
#4 ...
Thread 2 (Thread 0x7ffff77fe640 (LWP 12451)):
#0 0x00007ffff7fa364c in __lll_lock_wait () from /lib64/libc.so.6
#1 0x00007ffff7fa0123 in pthread_mutex_lock () from /lib64/libc.so.6
#2 0x0000000000401341 in std::mutex::lock (this=0x4040e0 <mutex_b>) at /usr/include/c++/v1/mutex:267
#3 0x00000000004011e5 in worker_job_1 () at worker_deadlock.cpp:15
#4 ...
3. Identifikasi Thread Pemilik Lock
Pindah ke frame fungsi yang memanggil lock di Thread 2, lalu cetak nilai internal struktur mutex untuk mencari LWP (Lightweight Process ID) pemilik lock:
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff77fe640 (LWP 12451))]
(gdb) print mutex_b.__data.__owner
$1 = 12452
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff6ffd640 (LWP 12452))]
(gdb) print mutex_a.__data.__owner
$2 = 12451
Hasil inspeksi membuktikan adanya circular wait:
- Thread 2 (LWP 12451) memegang
mutex_adan menunggumutex_b. - Thread 3 (LWP 12452) memegang
mutex_bdan menunggumutex_a.
Solusi: Standardisasi Urutan Penguncian dan Timeout
Dua pendekatan berikut menyelesaikan masalah lock contention tanpa risiko silent hang:
Pendekatan 1: Lock Hierarchy Menggunakan std::scoped_lock
Di C++17, gunakan std::scoped_lock untuk mengunci banyak mutex secara atomik tanpa risiko deadlock, menggunakan algoritma deadlock avoidance (seperti perbandingan alamat memori internal pointer mutex):
// Solusi urutan lock aman
void worker_job_1() {
std::cout << "[Worker 1] Mengunci mutex_a dan mutex_b secara bersamaan...\n";
std::scoped_lock lock(mutex_a, mutex_b);
// Jalankan pemrosesan antrean
}
void worker_job_2() {
std::cout << "[Worker 2] Mengunci mutex_a dan mutex_b secara bersamaan...\n";
std::scoped_lock lock(mutex_a, mutex_b); // Tetap aman walau urutan pemanggilan di kode sama
// Jalankan pemrosesan antrean
}
Pendekatan 2: Mekanisme Timeout via std::unique_lock dan std::timed_mutex
Jika resource harus diambil secara bertahap, hindari pemblokiran tak terbatas. Gunakan std::timed_mutex dengan try_lock_for agar worker dapat melepaskan resource yang telah diambil dan mengembalikan job ke antrean saat batas waktu tercapai:
// Solusi timeout untuk mencegah silent hang
#include <mutex>
#include <chrono>
std::timed_mutex timed_mutex_a;
std::timed_mutex timed_mutex_b;
void worker_job_with_timeout() {
std::unique_lock<std::timed_mutex> lock_a(timed_mutex_a, std::chrono::seconds(2));
if (!lock_a.owns_lock()) {
std::cerr << "[Timeout] Gagal mengakuisisi lock_a, membatalkan job.\n";
return;
}
std::unique_lock<std::timed_mutex> lock_b(timed_mutex_b, std::chrono::seconds(2));
if (!lock_b.owns_lock()) {
std::cerr << "[Timeout] Gagal mengakuisisi lock_b. Mengembalikan lock_a ke pool.\n";
return; // lock_a otomatis dirilis di sini via RAII
}
// Jalankan tugas kritis
}
Verifikasi Perbaikan
- Kompilasi ulang kode yang telah diperbaiki.
- Jalankan beban pengujian secara paralel dengan jumlah thread tinggi:
for i in {1..20}; do ./worker_fixed & done
- Periksa status proses di OS menggunakan
psatautop: pastikan tidak ada proses yang tertahan pada statusD(uninterruptible sleep) atauSkonstan tanpa pergantian state pemrosesan antrean. - Uji ulang dengan GDB: perintah
thread apply all btharus menunjukkan thread berada pada idle state antrean (seperticondition_variable::wait) saat queue kosong, bukan tertahan padapthread_mutex_lockantar-resource.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!