Перейти к содержанию

Последнее обновление: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()); 
}
Видите, этот код гораздо сложнее приведённого выше сценария: вложенность минимум в 5 уровней стека, и остальной код я даже не показывал, но по сути обе ситуации — одна и та же модель дедлока.

2. Отладка

Разобрав модель взаимоблокировки, выясняем: как локализовать такую проблему?

Есть два способа. Первый — запустить программу напрямую, а затем подключиться gdb.

Например:

./a.out
Затем, найдя номер процесса:

gdb -p xxx
Теперь мы можем увидеть потоки, ожидающие захвата блокировки.

(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
На этом всё; если было интересно — делитесь этой хардкорной статьёй! И заодно порекомендую свой проект пула потоков: я лично разобрал его в видео и снабдил развертыванием в docker одной командой (это видео я позже планирую записать для всех) — заинтересованным пишите мне.

Похожие темы

Комментарии