Что такое spinlock? Когда его стоит использовать вместо мьютекса?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Spinlock (Спин-блокировка)
Определение
Spinlock — это примитив синхронизации, который занят-ждёт (busy-wait) в цикле, проверяя флаг блокировки, вместо того чтобы спать (как мьютекс). Поток постоянно вращается в цикле, проверяя, не освободилась ли блокировка.
В отличие от мьютекса, спинлок не переводит поток в сон, а держит его в цикле проверки.
Концепция
Мьютекс (спит):
Поток 1: Захватил мьютекс → работа → освободил
Поток 2: Пытается захватить → СПИТ (context switch) → просыпается → работа
ОС: Может работать с другими потоками
Spinlock (вращается):
Поток 1: Захватил spinlock → работа → освободил
Поток 2: Пытается захватить → ВРАЩАЕТСЯ в цикле (context switch НЕ происходит)
ОС: Поток 2 занимает CPU 100%
Реализация spinlock
Базовая реализация
#include <atomic>
#include <thread>
using namespace std;
class SimpleSpinlock {
private:
atomic<bool> locked{false};
public:
void lock() {
// Вращаемся в цикле, пока не захватим
while (locked.exchange(true, memory_order_acquire)) {
// Занято, продолжаем ждать
}
}
void unlock() {
locked.store(false, memory_order_release);
}
};
int main() {
SimpleSpinlock lock;
int shared = 0;
auto worker = [&]() {
for (int i = 0; i < 1000; ++i) {
lock.lock();
shared++;
lock.unlock();
}
};
thread t1(worker);
thread t2(worker);
t1.join();
t2.join();
cout << "Result: " << shared << endl; // 2000
return 0;
}
Оптимизированный spinlock с pause
Без оптимизации spinlock буквально сжигает CPU. Добавим pause инструкцию (x86-64):
#include <atomic>
#include <thread>
#include <immintrin.h> // Для _mm_pause()
using namespace std;
class OptimizedSpinlock {
private:
atomic<bool> locked{false};
public:
void lock() {
while (locked.exchange(true, memory_order_acquire)) {
_mm_pause(); // Подсказка CPU: спец инструкция для ожидания
}
}
void unlock() {
locked.store(false, memory_order_release);
}
};
Spinlock с exponential backoff
#include <atomic>
#include <thread>
using namespace std;
class BackoffSpinlock {
private:
atomic<bool> locked{false};
public:
void lock() {
int spins = 0;
const int MAX_SPINS = 32;
while (locked.exchange(true, memory_order_acquire)) {
if (spins < MAX_SPINS) {
// Малое ожидание: спинимся
for (int i = 0; i < (1 << spins); ++i) {
__asm__("pause"); // x86 инструкция
}
++spins;
} else {
// После MAX_SPINS даём другому потоку время
this_thread::yield(); // Выход из цикла, но не спим
}
}
}
void unlock() {
locked.store(false, memory_order_release);
}
};
Сравнение: Spinlock vs Мьютекс
| Характеристика | Spinlock | Мьютекс |
|---|---|---|
| Ожидание | Спинится в цикле | Спит (context switch) |
| CPU использование | 100% (активное ожидание) | Минимальное (спит) |
| Контекст-переключение | Нет | Да (дорого!) |
| Время захвата | Очень быстро | Медленнее (переключение) |
| Для коротких блокировок | Отличный выбор | Может быть неэффективен |
| Для длинных блокировок | Ужасен (сжигает CPU) | Оптимален |
| Справедливость (fairness) | Нет (голодание потоков) | Да |
Практические примеры
Пример 1: Сверхбыстрый счётчик
#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
using namespace std;
class Counter {
private:
long count = 0;
atomic<bool> lock{false};
public:
void increment() {
// Spinlock для счётчика (очень короткая критическая секция)
while (lock.exchange(true, memory_order_acquire));
count++;
lock.store(false, memory_order_release);
}
long getCount() const { return count; }
};
int main() {
Counter counter;
vector<thread> threads;
auto start = chrono::high_resolution_clock::now();
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < 1000000; ++j)
counter.increment();
});
}
for (auto& t : threads) t.join();
auto end = chrono::high_resolution_clock::now();
auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
cout << "Count: " << counter.getCount() << endl;
cout << "Time: " << duration.count() << "ms" << endl;
return 0;
}
Пример 2: Lock-free очередь со spinlock
#include <atomic>
#include <queue>
using namespace std;
template<typename T>
class SpinlockQueue {
private:
queue<T> q;
atomic<bool> spinlock{false};
public:
void enqueue(const T& value) {
while (spinlock.exchange(true, memory_order_acquire));
q.push(value);
spinlock.store(false, memory_order_release);
}
bool dequeue(T& value) {
while (spinlock.exchange(true, memory_order_acquire));
bool success = !q.empty();
if (success) {
value = q.front();
q.pop();
}
spinlock.store(false, memory_order_release);
return success;
}
};
Когда использовать spinlock
Spinlock хорош для:
-
Очень короткие критические секции (< 1 микросекунда)
spinlock.lock(); counter++; // Одна операция spinlock.unlock(); -
High-performance системы (финтех, торговля, HFT)
- Каждая микросекунда считается
- Предсказуемость важнее экономии CPU
-
Real-time системы (RTOS)
- Нужны гарантии по времени ответа
- Context switch недопустим
-
Многоядерные системы (когда каждый поток на отдельном ядре)
- Context switch дорогой
- Spilling вероятен: другой поток скоро освободит блокировку
Когда НЕ использовать spinlock
Мьютекс лучше для:
-
Длительные операции в критической секции
spinlock.lock(); sleep(1); // Плохо! Spinlock вращается целую секунду spinlock.unlock(); -
Однопоточные приложения — вечное спинирование!
-
Неизвестна длительность блокировки
- Может быть как коротко, так и долго
- Мьютекс универсален
-
Мало ядер или высокая контенция (много потоков борются за одну блокировку)
- Context switch случится в любом случае
- Спинирование только сжигает CPU зря
Анализ производительности
Сценарий 1: Низкая контенция, короткие блокировки
Крит. секция: 100ns (0.0001ms)
Время контекст-переключения: 1000ns (0.001ms)
Mutex: ~1000ns (переключение) [ПЛОХО]
Spinlock: ~100ns (вращение) [ХОРОШО]
Викторь: Spinlock на 10x быстрее!
Сценарий 2: Высокая контенция, длительные блокировки
Крит. секция: 10000ns (0.01ms)
Время контекст-переключения: 1000ns (0.001ms)
Количество конкурирующих потоков: 8
Mutex: ~1000ns + очередь ожидания [ХОРОШО — спит]
Spinlock: ~80000ns (вращение) [УЖАСНО — сжигает CPU]
C++ 17: std::atomic spin lock
В C++17 появился встроенный спинлок через std::atomic:
#include <atomic>
#include <thread>
using namespace std;
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void critical_section() {
// Спинлок на atomics
while (lock.test_and_set(memory_order_acquire)) {
// Спинируемся
}
// Критическая секция
lock.clear(memory_order_release);
}
C++20 добавил atomic::wait() для более эффективного ожидания:
#include <atomic>
#include <thread>
using namespace std;
std::atomic<bool> lock{false};
void better_lock() {
while (lock.exchange(true, memory_order_acquire)) {
lock.wait(true, memory_order_relaxed); // Более эффективно!
}
}
Best practices
Правило большого пальца:
- Если вы не уверены, используйте мьютекс
- Если вы профилировали и знаете, что блокировка очень короткая, попробуйте spinlock
- Для real-time и HFT систем spinlock часто обязателен
Итоговые рекомендации
Spinlock — это инструмент для экстремально низколатентных систем:
- Обменивает CPU на низкую задержку — спинирование вместо спанияания
- Отличен для коротких блокировок — когда spinirование быстрее контекст-переключения
- Опасен в неправильных ситуациях — может сжечь весь CPU
- Требует профилирования — не гадайте, измеряйте!
- HFT/финтех стандарт — основной выбор для финансовых систем
Мастерство выбора между spinlock и мьютексом — это признак опытного backend разработчика, понимающего оборудование и операционные системы.