В чем разница между Mutex и Spinlock?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между Mutex и Spinlock: разные стратегии ожидания
Оба механизма синхронизации служат для защиты критических секций, но используют кардинально разные подходы к ожиданию блокировки. Выбор между ними зависит от времени ожидания и типа системы.
Что такое Mutex (Mutual Exclusion)
Mutex — блокировка, которая переводит поток в сонное состояние, если ресурс занят.
#include <mutex>
#include <thread>
std::mutex m;
int shared_counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(m); // Заблокировать
shared_counter++; // Критическая секция
} // Разблокировать (lock_guard деструктор)
Как работает:
- Поток пытается захватить mutex
- Если свободен — захватывает и продолжает
- Если занят — поток уходит в очередь и переводится в режим ожидания (sleep)
- ОС пробуждает поток когда mutex освобождается
Что такое Spinlock
Spinlock — блокировка, которая активно ожидает в цикле (busy-waiting).
struct Spinlock {
std::atomic<bool> flag = false;
void lock() {
// Крутимся в цикле пока не захватим
while (flag.exchange(true, std::memory_order_acquire)) {
// Активное ожидание - поток НЕ спит!
}
}
void unlock() {
flag.store(false, std::memory_order_release);
}
};
Spinlock spin_lock;
void critical_section() {
spin_lock.lock();
// ... работа ...
spin_lock.unlock();
}
Как работает:
- Поток пытается захватить spinlock
- Если свободен — захватывает
- Если занят — поток остаётся активным и постоянно проверяет флаг
- Это потребляет CPU, но даёт быстрый доступ когда ресурс освобождается
Сравнение основных характеристик
Поведение при блокировке:
- Mutex: поток спит, не потребляет CPU ✓
- Spinlock: поток активно крутится, потребляет CPU ✗
Задержка захвата после освобождения:
- Mutex: может быть задержка на пробуждение (микросекунды-миллисекунды)
- Spinlock: минимальная задержка (наносекунды)
Потребление ресурсов:
- Mutex: низкое при длительном ожидании
- Spinlock: высокое (топит CPU)
Справедливость (fairness):
- Mutex: гарантирует FIFO очередь
- Spinlock: непредсказуемо, может "прыгнуть в очередь"
Когда использовать Mutex
1. Ожидание может быть долгим
std::mutex io_lock;
void read_from_disk() {
std::lock_guard<std::mutex> lock(io_lock);
// I/O операция может занять миллисекунды
// Нет смысла жечь CPU в spinlock
disk.read();
}
2. Много потоков конкурируют
std::mutex m;
// 100 потоков ждут одного mutex
// Spinlock было бы disaster - 99% CPU для ничего
for (int i = 0; i < 100; i++) {
std::lock_guard<std::mutex> lock(m);
// работа
}
3. Переменное время ожидания
std::mutex cache_lock;
void access_cache() {
std::lock_guard<std::mutex> lock(cache_lock);
// Иногда 1 микросекунда, иногда 100 миллисекунд
// Mutex справляется лучше
}
Когда использовать Spinlock
1. Критическая секция очень короткая
Spinlock ref_count_lock;
int ref_count = 0;
void increment_ref() {
spin_lock.lock();
ref_count++; // 1-2 инструкции - микросекунда
spin_lock.unlock();
// Переключение контекста дороже, чем spinning
}
2. Ожидание почти всегда короткое
Spinlock update_lock;
void update_counter() {
spin_lock.lock();
// Обычно занято < 100 наносекунд
counter++;
spin_lock.unlock();
}
3. System programming / Real-time
// В kernel code или real-time системах
// где нельзя позволить себе context switch
Spinlock kernel_lock;
void kernel_critical() {
spin_lock.lock();
// Должно быть очень быстро
// и предсказуемо по времени
spin_lock.unlock();
}
4. Системы с малым числом потоков
// 2-4 потока на 4 ядрах
// Каждый может крутиться на своём ядре
// Нет проблемы "голодного" процесса
Spinlock lock;
Практические примеры
Плохо: Spinlock на I/O операции
Spinlock bad_lock; // ❌ Криминально плохо
void read_file() {
bad_lock.lock();
// Ожидание I/O может быть 10ms
// Поток жрёт 100% CPU впустую
file.read();
bad_lock.unlock();
}
Хорошо: Spinlock на микроскопической операции
Spinlock good_lock; // ✓ Имеет смысл
std::atomic<int> counter = 0;
void increment() {
good_lock.lock();
counter++; // ~5 наносекунд
good_lock.unlock();
// Переключение контекста дороже
}
C++ стандартные инструменты
Для Mutex:
#include <mutex>
std::mutex m;
std::lock_guard<std::mutex> lock(m); // RAII автоматик
std::unique_lock<std::mutex> ulock(m); // С возможностью разблокировать
std::scoped_lock multi_lock(m1, m2, m3); // Несколько mutex одновременно
Для Spinlock:
#include <atomic>
#include <thread>
class Spinlock {
private:
std::atomic<bool> locked = false;
public:
void lock() {
while (locked.exchange(true, std::memory_order_acquire)) {
std::this_thread::yield(); // Подсказка ОС: дай ход другому потоку
}
}
void unlock() {
locked.store(false, std::memory_order_release);
}
};
Гибридный подход: Hybrid Lock
Можно комбинировать оба подхода:
class HybridLock {
private:
std::atomic<int> state = 0;
std::mutex fallback;
public:
void lock() {
// Сначала попробуем spinlock
for (int i = 0; i < 100; i++) {
if (state.compare_exchange_weak(0, 1)) {
return; // Захватили быстро
}
std::this_thread::yield();
}
// Если не удалось за 100 итераций - mutex
fallback.lock();
}
void unlock() {
state.store(0);
}
};
Правило для выбора
Время в критической секции < 10 микросекунд? → Spinlock
Время в критической секции > 100 микросекунд? → Mutex
10-100 микросекунд? → Смотря на ситуацию, может быть гибридный
Вывод
- Mutex — универсальный выбор для большинства случаев
- Spinlock — для очень коротких критических секций
- По умолчанию начни с Mutex — это безопаснее для CPU
- Профилируй перед оптимизацией, не гадай
- В многопоточных приложениях Mutex почти всегда правильный выбор