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

Какие знаешь мьютексы?

2.0 Middle🔥 132 комментариев
#Многопоточность и синхронизация

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

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

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

Какие знаешь мьютексы?

Мьютексы (mutual exclusion) — это основа синхронизации в многопоточном коде. За 10+ лет я работал с разными видами мьютексов и их комбинациями. Опишу каждый и как их правильно использовать.

1. std::mutex — базовый мьютекс

Самый простой вид. Может быть в двух состояниях: заблокирован или разблокирован.

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 1000; i++) {
        std::lock_guard<std::mutex> lock(mtx);  // Автоматическая блокировка
        counter++;  // Критичный раздел
    }  // Автоматическая разблокировка
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // 2000 (безопасно)
}

Без мьютекса результат был бы случайным (race condition):

// ОПАСНО!
void increment_unsafe() {
    for (int i = 0; i < 1000; i++) {
        counter++;  // NOT atomic!
        // На самом деле:
        // 1. Читаем counter
        // 2. Увеличиваем
        // 3. Пишем обратно
        // Между шагами другой поток может изменить counter!
    }
}
// Результат может быть 1500, 1234, 1999, не 2000

2. std::lock_guard vs std::unique_lock

std::lock_guard — простой и быстрый

std::mutex mtx;

void func() {
    std::lock_guard<std::mutex> lock(mtx);  // Locked
    // Критичный раздел
}  // Unlocked автоматически

Преимущества:

  • Простота
  • Нулевые накладные расходы
  • RAII гарантия

Недостатки:

  • Нельзя явно разблокировать
  • Нельзя использовать условные переменные

std::unique_lock — гибкий

std::mutex mtx;
std::condition_variable cv;  // Условная переменная
bool ready = false;

void wait_for_signal() {
    std::unique_lock<std::mutex> lock(mtx);  // Locked
    
    // Ждём сигнала от другого потока
    cv.wait(lock, []{ return ready; });
    
    std::cout << "Signal received!" << std::endl;
}  // Unlocked автоматически

void send_signal() {
    {
        std::unique_lock<std::mutex> lock(mtx);
        ready = true;
    }  // Unlocked перед notify
    
    cv.notify_one();  // Пробуждаем один ждущий поток
}

Преимущества:

  • Явная разблокировка (unlock())
  • Поддержка condition_variable
  • Defer lock (задержанная блокировка)
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// lock еще не захвачен

lock.lock();      // Явная блокировка
lock.unlock();    // Явная разблокировка
lock.lock();      // Можем заблокировать снова

3. std::recursive_mutex — рекурсивный мьютекс

Один поток может заблокировать мьютекс несколько раз.

std::recursive_mutex rmtx;

void process(int level) {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    std::cout << "Level " << level << std::endl;
    
    if (level < 3) {
        process(level + 1);  // Рекурсивный вызов
        // Нет deadlock'а!
    }
}

int main() {
    process(1);
    // Level 1
    // Level 2
    // Level 3
}

ВАЖНО: Избегайте рекурсивных мьютексов если можно. Это признак плохого дизайна.

// ❌ ПЛОХО
class Counter {
private:
    std::recursive_mutex mtx;  // Признак проблемы
    int value = 0;
    
public:
    void increment() {
        std::lock_guard<std::recursive_mutex> lock(mtx);
        value++;
    }
    
    int get_double() {
        std::lock_guard<std::recursive_mutex> lock(mtx);
        return value * 2;
    }
};

// ✅ ХОРОШО
class Counter {
private:
    std::mutex mtx;
    int value = 0;
    
    // Приватный метод без блокировки
    int get_value_unlocked() const {
        return value;
    }
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        value++;
    }
    
    int get_double() {
        std::lock_guard<std::mutex> lock(mtx);
        return get_value_unlocked() * 2;  // Уже заблокировано
    }
};

4. std::timed_mutex и std::recursive_timed_mutex

Мьютексы с timeout.

std::timed_mutex tmtx;

bool try_critical_section() {
    // Пытаемся заблокировать на 1 секунду
    if (tmtx.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "Lock acquired!" << std::endl;
        // Критичный раздел
        tmtx.unlock();
        return true;
    } else {
        std::cout << "Lock timeout!" << std::endl;
        return false;
    }
}

bool try_until_time() {
    auto deadline = std::chrono::high_resolution_clock::now() + std::chrono::seconds(5);
    
    if (tmtx.try_lock_until(deadline)) {
        // Успешно заблокировали до deadline
        tmtx.unlock();
        return true;
    }
    return false;
}

5. std::shared_mutex (читатели-писатели)

Несколько читателей могут одновременно захватить мьютекс, но писатель требует эксклюзивного доступа.

std::shared_mutex rwmtx;  // Reader-Writer mutex
int shared_data = 0;

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(rwmtx);
    std::cout << "Reader " << id << " sees: " << shared_data << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

void writer(int id, int value) {
    std::unique_lock<std::shared_mutex> lock(rwmtx);
    std::cout << "Writer " << id << " sets: " << value << std::endl;
    shared_data = value;
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
}

int main() {
    std::thread readers[3], writers[2];
    
    // Запускаем 3 читателя
    for (int i = 0; i < 3; i++) {
        readers[i] = std::thread(reader, i);
    }
    
    // Запускаем 2 писателя
    for (int i = 0; i < 2; i++) {
        writers[i] = std::thread(writer, i, i * 10);
    }
    
    for (int i = 0; i < 3; i++) readers[i].join();
    for (int i = 0; i < 2; i++) writers[i].join();
}

Timeline:

Reader 0, Reader 1, Reader 2 читают одновременно (shared lock)
  ↓
Writer 0 ждёт (нужен exclusive lock)
  ↓
Reader 0, 1, 2 завершают
  ↓
Writer 0 захватывает и пишет
  ↓
Writer 1 ждёт
  ↓
Writer 0 завершает
  ↓
Writer 1 захватывает и пишет

6. std::condition_variable

Уведомление между потоками.

std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;

void producer() {
    for (int i = 0; i < 5; i++) {
        {
            std::unique_lock<std::mutex> lock(mtx);
            queue.push(i);
            std::cout << "Produced: " << i << std::endl;
        }  // lock释放
        
        cv.notify_one();  // Пробуждаем один потребителя
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer(int id) {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // Ждём, пока очередь не пуста
        cv.wait(lock, []{ return !queue.empty(); });
        
        if (queue.empty()) break;
        
        int value = queue.front();
        queue.pop();
        std::cout << "Consumer " << id << " consumed: " << value << std::endl;
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons1(consumer, 1);
    std::thread cons2(consumer, 2);
    
    prod.join();
    // ...
}

7. Deadlock и как его избежать

Deadlock — ситуация, когда потоки ждут друг друга бесконечно.

// ❌ ОПАСНО — возможен deadlock
std::mutex mtx1, mtx2;

void thread1_func() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock2(mtx2);  // Ждём mtx2
}

void thread2_func() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock1(mtx1);  // Ждём mtx1
}

// thread1 владеет mtx1, ждёт mtx2
// thread2 владеет mtx2, ждёт mtx1
// DEADLOCK!

Решение: std::lock с несколькими мьютексами

// ✅ БЕЗОПАСНО
std::mutex mtx1, mtx2;

void thread1_func() {
    std::lock(mtx1, mtx2);  // Блокируем оба сразу в безопасном порядке
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    // Логика
}

void thread2_func() {
    std::lock(mtx1, mtx2);  // Тот же порядок — no deadlock
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    // Логика
}

8. Практический выбор мьютекса

┌─────────────────────┬──────────────────┬────────────────────┐
│    Тип мьютекса     │    Использование │   Производительность│
├─────────────────────┼──────────────────┼────────────────────┤
│ std::mutex          │ Общие случаи     │ Быстро             │
│ std::timed_mutex    │ С timeout        │ Средне             │
│ std::recursive_mx.  │ Редко (плохо)    │ Медленно            │
│ std::shared_mutex   │ Много читателей  │ Зависит от нагрузки│
└─────────────────────┴──────────────────┴────────────────────┘

9. Lock-free альтернатива

// Вместо мьютекса можно использовать атомы
std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 1000; i++) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // 2000
}

Вывод

Правила использования мьютексов:

  1. Используй std::lock_guard по умолчанию
  2. Используй std::unique_lock если нужна condition_variable
  3. Избегай std::recursive_mutex (признак плохого дизайна)
  4. Используй std::shared_mutex для читателей-писателей
  5. Блокируй минимальное количество кода
  6. Избегай вложенных блокировок (или используй std::lock)
  7. Рассмотри lock-free структуры для критичного по производительности кода