Что такое SpinLock (спинлок) в многопоточности?

Ответ

SpinLock (блокировка с активным ожиданием) — это низкоуровневый примитив синхронизации, в котором поток, не сумевший захватить блокировку, не переходит в состояние ожидания ядра ОС, а продолжает активно опрашивать ("крутиться" в цикле) флаг доступности.

Когда использовать:

  • Короткие критические секции (несколько десятков или сотен инструкций).
  • Минимизация накладных расходов на переключение контекста, когда время ожидания предположительно мало.
  • В системном программировании, при реализации собственных примитивов или в ядре ОС.

Недостатки:

  • Потребление процессорного времени: Поток в цикле ожидания занимает ядро ЦП (busy-waiting).
  • Неэффективность: При длительном ожидании бесполезно тратит процессорные такты.

Реализация на C++ с использованием std::atomic_flag:

#include <atomic>

class SpinLock {
    std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        // test_and_set() атомарно устанавливает флаг в true и возвращает предыдущее значение.
        // Цикл продолжается, пока предыдущее значение было true (заблокировано).
        while (lock_flag.test_and_set(std::memory_order_acquire)) {
            // На многих архитекциях полезно дать подсказку процессору,
            // что это цикл ожидания (может снизить энергопотребление/конфликт шины).
            // __builtin_ia32_pause(); // Для x86
        }
    }

    void unlock() {
        // Атомарно сбрасываем флаг, разрешая другим потокам захватить блокировку.
        lock_flag.clear(std::memory_order_release);
    }
};

// Использование
SpinLock my_lock;
int shared_data = 0;

void thread_function() {
    my_lock.lock();
    ++shared_data; // Критическая секция
    my_lock.unlock();
}

На практике в пользовательском коде чаще используют std::mutex, который эффективнее для блокировок средней и большой длительности, так как передаёт управление планировщику ОС.

Ответ 18+ 🔞

А, SpinLock, говоришь? Ну это ж классика, ебать мои старые костыли! Представь себе: один поток влез в твои общие данные, а другой стоит под дверью и не просто ждёт, а тупо долбится лбом в косяк, пока первый не выйдет. Вот это и есть активное ожидание, или busy-waiting, если по-умному.

Так когда же эту штуку впиндюривать?

  • Когда твоя критическая секция — короткая, как шаг пигалицы. Буквально пару сотен операций. Поток управится быстрее, чем ОС успеет его усыпить и разбудить.
  • Когда тебе овердохуища важна скорость, а не вежливость. Чтобы не платить за переключение контекста, если ждать-то всего микросекунды.
  • В ядре системы или когда ты пишешь свои, ещё более хитрые, примитивы синхронизации. Там это обычное дело.

А где подвох?

  • Жрёт процессорное время, как не в себя. Пока один крутится в цикле, ядро ЦП у него занято на все сто. Это как если бы охранник на проходной вместо того чтобы сидеть на стуле, непрерывно приседал, ожидая пропуск.
  • Полная неэффективность на длинных дистанциях. Если поток надолго засел внутри, то остальные будут просто жечь электричество впустую, терпения ноль ебать.

Вот, смотри, как это выглядит в коде на C++ с std::atomic_flag:

#include <atomic>

class SpinLock {
    std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        // test_and_set() — атомарно ставит флаг в true и возвращает, что было ДО этого.
        // Крутимся, пока кто-то уже не поставил true (заблокировал).
        while (lock_flag.test_and_set(std::memory_order_acquire)) {
            // Тут можно дать процессору намёк, что мы просто ждём.
            // Типа, расслабься, не гони так шину. Для x86 это _mm_pause().
            // __builtin_ia32_pause();
        }
    }

    void unlock() {
        // Просто сбрасываем флажок. Всё, проезд свободен.
        lock_flag.clear(std::memory_order_release);
    }
};

// Использование
SpinLock my_lock;
int shared_data = 0;

void thread_function() {
    my_lock.lock();
    ++shared_data; // Делаем что-то важное в одиночестве
    my_lock.unlock();
}

А в жизни-то что? Если ты не пишешь ядро или супер-оптимизированную библиотеку, то забей хуй и юзай обычный std::mutex. Он умный: если не получается зайти, поток засыпает, не жрёт цпу, и ОС будит его, когда надо. Для всего, что длиннее пары тактов, это правильный путь. Не изобретай велосипед, где можно проехать на трамвае.