EnglishРусский Map

Spinlock

title
Spinlock
type
concept
summary
Блокировка через активное ожидание в цикле вместо сна; дёшево для коротких критических секций и катастрофично для длинных
tags
concurrency, kernel, performance
created
2026-04-30
updated
2026-04-30
lang
ru
translation_of
spinlock
source_updated
2026-04-30
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-high

Spinlock - это простейший примитив взаимного исключения:

while (!try_acquire_lock()) {
    /* keep checking */
}

Ожидающий поток не засыпает. Он сжигает процессорное время в плотном цикле, пока блокировка не освободится.

Когда это правильный выбор

Усыпление потока и его последующее пробуждение стоят от сотен наносекунд до микросекунд. Если критическая секция короче этого времени, засыпать дороже, чем крутиться в цикле. Короткие, быстрые, предсказуемые критические секции - пара обновлений указателей, атомарный счётчик - ровно то, для чего нужны spinlock'и.

Вся эта схема держится на одном допущении: владелец освободит блокировку очень скоро. Никто не станет вытеснять поток посреди 20-наносекундной секции. Владелец завершит работу раньше, чем кто-либо заметит.

Когда допущение ломается

Любая причина, превращающая "20-наносекундную" секцию во что-то более длинное, ломает всю математику:

  • Page fault внутри критической секции. Ядру приходится выделять физическую страницу и обновлять mapping - это микросекунды, а не наносекунды.
  • Syscall на медленном пути.
  • Вытеснение планировщиком, особенно пока владелец находится в процессе обработки fault'а. См. linux-preemption-models.

Когда это происходит, цена - не просто "один поток подождал дольше". Это t × N, где N - число ожидающих в цикле потоков. На машине с 96 vCPU и сотнями конкурирующих backend'ов такой множитель намертво сжигает CPU.

Канонический пример - linux-7-postgres-regression: StrategyGetBuffer в PostgreSQL держит глобальный spinlock, обращаясь к разделяемой памяти, которая может быть ещё не отображена. При PREEMPT_NONE владелец завершал обработку fault'а и отпускал блокировку. При PREEMPT_LAZY планировщик может вытеснить его прямо посреди fault'а, умножая время ожидания на каждый ждущий backend.

Способы защиты

  • Делать критическую секцию по-настоящему короткой - никаких системных вызовов и обращений к невыделенной памяти (first-touch).
  • Заранее вызывать page fault и отображать память, к которой обращается секция (например, huge-pages на порядки снижают число first-touch fault'ов).
  • Использовать Restartable Sequences (rseq), чтобы ядро сигнализировало о вытеснении, а userspace мог перезапустить секцию. Специфично для Linux.
  • Переходить на засыпающий mutex, когда конкуренция или длина секции выходят за рамки того, где spinlock эффективен.