Последнее обновление:2026-10-12
Поиск взаимоблокировок в C++¶
Как в C++ локализовать взаимоблокировки при параллельном программировании?¶
Недавно я писал для Apache arrow параллельный оператор nested loop join; а где параллельность — там неизбежно пресловутая проблема взаимоблокировок. Предположим, вы столкнулись с дедлоком — как отлаживать и локализовать его?
Это и есть цель данной статьи: помочь вам быстро освоить параллельное программирование в части обнаружения и отладки дедлоков. Всё предельно концентрированно, переходим к сути.
1. Введение¶
Чтобы лучше объяснить взаимоблокировку, начнём с программы.
std::mutex gMutex;
int t2() {
std::lock_guard<std::mutex> m(gMutex);
return 0;
}
int t1() {
std::lock_guard<std::mutex> l(gMutex);
return t2();
}
t1(), и в функции t2() выполняется захват глобального мьютекса gMutex, причём функция t1() после захвата вызывает функцию t2(), внутри которой снова предпринимается попытка захватить gMutex.
Блокировка t1 уже взята и ещё не освобождена, а t2 пытается захватить её снова — обе стороны ждут, кто освободит первым, и попадают в бесконечное ожидание. В реальном проекте код, конечно, не так прост, а очень сложен; например, код, который я писал в Apache arrow, выглядит так:
Status OnBuildSideFinished(size_t thread_index) {
std::lock_guard<std::mutex> guard(probe_side_mutex_);
// do something
accumulate_build_ready_ = true;
return scheduler_->StartTaskGroup(thread_index,task_group_probe_,queued_batches_to_probe_.batch_count());
}
2. Отладка¶
Разобрав модель взаимоблокировки, выясняем: как локализовать такую проблему?
Есть два способа. Первый — запустить программу напрямую, а затем подключиться gdb.
Например:
Затем, найдя номер процесса: Теперь мы можем увидеть потоки, ожидающие захвата блокировки.(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fe2740 (LWP 32301) "a.out" 0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0
2 Thread 0x7ffff6fd0700 (LWP 32305) "a.out" 0x00007ffff7bcd54d in __lll_lock_wait () from /lib64/libpthread.so.0
__lll_lock_wait: например, я посмотрел поток 2, затем по стеку нашёл номера строк t1 и t2 — сразу видно, где проблема, очень наглядно!
Помимо этого способа, можно сразу запустить программу под gdb: она зависнет, после чего ctrl + c прерывает её — и получим то же самое, что и выше.
Например:
(gdb) r
Starting program: /home/light/a.out
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Hello World!
[New Thread 0x7ffff6fd0700 (LWP 32305)]
^C
Thread 1 "a.out" received signal SIGINT, Interrupt.
0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0
Missing separate debuginfos, use: debuginfo-install libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fe2740 (LWP 32301) "a.out" 0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0
2 Thread 0x7ffff6fd0700 (LWP 32305) "a.out" 0x00007ffff7bcd54d in __lll_lock_wait () from /lib64/libpthread.so.0
Похожие темы¶
- Путеводитель по C++ Concurrency in Action | C++那些事
- Векторизация C++ SIMD | C++那些事
- Параллельный STL в C++17 | C++那些事
- C++ False Sharing и строка кэша | C++那些事
- Отладка многопоточности GDB в режиме All-Stop | C++那些事
- Обнаружение ошибок конкурентности с помощью Helgrind | C++那些事
- Принцип событийного цикла libev | C++那些事
- Внутренняя реализация C++ std::thread | C++那些事
- C++ std::thread и jthread | C++那些事
- Введение в многопоточность C++ | C++那些事