Почему случается deadlock?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему случается deadlock?
Deadlock — одна из самых коварных проблем в многопоточном программировании и в БД. Это происходит, когда система попадает в состояние циклической зависимости блокировок, откуда нет выхода.
Классический сценарий deadlock'а
Поток 1:
- Захватывает мьютекс A
- Пытается захватить мьютекс B
Поток 2:
- Захватывает мьютекс B
- Пытается захватить мьютекс A
Результат: оба потока ждут друг друга вечно.
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mutex_a, mutex_b;
int resource_a = 0, resource_b = 0;
void thread1_func() {
std::lock_guard<std::mutex> lock_a(mutex_a);
std::cout << "Thread 1: locked A\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Даём шанс другому потоку
std::lock_guard<std::mutex> lock_b(mutex_b); // DEADLOCK: ждёт B
resource_a++;
resource_b++;
}
void thread2_func() {
std::lock_guard<std::mutex> lock_b(mutex_b);
std::cout << "Thread 2: locked B\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock_a(mutex_a); // DEADLOCK: ждёт A
resource_a++;
resource_b++;
}
int main() {
std::thread t1(thread1_func);
std::thread t2(thread2_func);
t1.join(); // Зависнет здесь
t2.join();
return 0;
}
Причины deadlock'а
1. Циклическая зависимость ресурсов
Это базовая причина. Если поток A удерживает ресурс X и ждёт ресурс Y, а поток B удерживает Y и ждёт X — deadlock.
Диаграмма ждиания (wait-for graph):
Поток 1 → Ресурс A (удерживает)
Поток 1 → Ресурс B (ждёт)
↑ удерживается Потоком 2
Поток 2 → Ресурс B (удерживает)
Поток 2 → Ресурс A (ждёт)
↑ удерживается Потоком 1
Цикл: 1→A→2→B→1 = DEADLOCK
2. Неправильный порядок захвата замков (lock ordering)
Если разные части кода захватывают замки в разных порядках, риск deadlock'а растёт.
// Функция 1: захватывает A, потом B
void function1() {
std::lock_guard<std::mutex> lock_a(mutex_a);
// ... делаем что-то ...
std::lock_guard<std::mutex> lock_b(mutex_b);
}
// Функция 2: захватывает B, потом A
void function2() {
std::lock_guard<std::mutex> lock_b(mutex_b);
// ... делаем что-то ...
std::lock_guard<std::mutex> lock_a(mutex_a); // DEADLOCK
}
3. Блокирующие операции внутри критической секции
Если поток, удерживающий замок, вызывает функцию, которая пытается захватить другой замок, и эта функция вызывается из другого контекста с другим порядком — deadlock.
class Account {
private:
std::mutex lock;
int balance = 1000;
public:
void transfer(Account& other, int amount) {
std::lock_guard<std::mutex> lock1(this->lock);
std::lock_guard<std::mutex> lock2(other.lock); // Потенциальный deadlock
balance -= amount;
other.balance += amount;
}
};
// Deadlock сценарий:
// Поток 1: account_a.transfer(account_b, 100) // Захватит A, ждёт B
// Поток 2: account_b.transfer(account_a, 50) // Захватит B, ждёт A
Deadlock в базах данных
В SQL deadlock случается, когда две транзакции блокируют друг друга:
-- Транзакция 1
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1; -- Захватила блокировку на row 1
UPDATE users SET balance = balance + 100 WHERE id = 2; -- Ждёт блокировку на row 2
COMMIT;
-- Транзакция 2 (параллельно)
BEGIN;
UPDATE users SET balance = balance - 50 WHERE id = 2; -- Захватила блокировку на row 2
UPDATE users SET balance = balance + 50 WHERE id = 1; -- Ждёт блокировку на row 1 (DEADLOCK!)
COMMIT;
Большинство БД (PostgreSQL, MySQL) обнаруживают deadlock и откатывают одну из транзакций.
Как предотвратить deadlock
1. Единообразный порядок захвата (lock ordering)
// ВСЕГДА захватывайте мьютексы в одном порядке
void thread1_safe() {
std::lock_guard<std::mutex> lock_a(mutex_a); // Первый A
std::lock_guard<std::mutex> lock_b(mutex_b); // Потом B
}
void thread2_safe() {
std::lock_guard<std::mutex> lock_a(mutex_a); // Первый A
std::lock_guard<std::mutex> lock_b(mutex_b); // Потом B
}
2. Использовать std::lock для захвата нескольких мьютексов
std::lock(mutex_a, mutex_b); // Захватывает атомарно, без deadlock
std::lock_guard<std::mutex> lock_a(mutex_a, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(mutex_b, std::adopt_lock);
3. Timeout'ы при захвате
std::unique_lock<std::mutex> lock(mutex, std::chrono::seconds(1));
if (!lock.owns_lock()) {
// Не удалось захватить за секунду, отступаем
return false;
}
4. Минимизировать критические секции
// ПЛОХО: держим замок долго
std::lock_guard<std::mutex> lock(mutex);
// ... много операций, включая блокирующие вызовы ...
// ХОРОШО: замок только где нужен
int value;
{
std::lock_guard<std::mutex> lock(mutex);
value = shared_data; // Только читаем
} // Замок отпущен
// ... теперь можно делать блокирующие операции без замка ...
5. Избегать nested lock'ов
// ПЛОХО
void function1() {
std::lock_guard<std::mutex> lock1(mutex1);
function2(); // Если function2 захватит другой мьютекс - риск
}
void function2() {
std::lock_guard<std::mutex> lock2(mutex2);
// ...
}
6. В БД: использовать правильные уровни изоляции
-- Используй READ COMMITTED для меньшего лока
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
7. Мониторинг и обнаружение
// Включить detection в БД
SHOW ENGINE INNODB STATUS; -- MySQL/InnoDB показывает deadlock'и
// В PostgreSQL
SELECT pid, mode, granted, query FROM pg_locks WHERE NOT granted;
Вывод
Deadlock — это результат циклической зависимости между ресурсами и потоками/транзакциями. Главная защита:
- Единообразный порядок захвата замков
- Минимальные критические секции
- Использовать std::lock для атомарного захвата
- Timeout'ы как последняя линия защиты
В production deadlock'ов быть не должно — это признак архитектурной проблемы, требующей рефакторинга.