← Назад к вопросам

Почему случается deadlock?

1.3 Junior🔥 111 комментариев
#Многопоточность и синхронизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему случается deadlock?

Deadlock — одна из самых коварных проблем в многопоточном программировании и в БД. Это происходит, когда система попадает в состояние циклической зависимости блокировок, откуда нет выхода.

Классический сценарий deadlock'а

Поток 1:

  1. Захватывает мьютекс A
  2. Пытается захватить мьютекс B

Поток 2:

  1. Захватывает мьютекс B
  2. Пытается захватить мьютекс 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 — это результат циклической зависимости между ресурсами и потоками/транзакциями. Главная защита:

  1. Единообразный порядок захвата замков
  2. Минимальные критические секции
  3. Использовать std::lock для атомарного захвата
  4. Timeout'ы как последняя линия защиты

В production deadlock'ов быть не должно — это признак архитектурной проблемы, требующей рефакторинга.