Spinlock
- title
- Spinlock
- type
- concept
- summary
- Блокировка через активное ожидание в цикле вместо сна; дёшево для коротких критических секций и катастрофично для длинных
- tags
- concurrency, kernel, performance
- sources
- linux-broke-postgresql
- 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 эффективен.
- The Art of Multiprocessor Programming
- Systems Performance
- Understanding Software Dynamics
- Compiler codegen luck — a cosmetic edit that ran 6x faster
- Why cache padding uses 128 bytes on a 64-byte cache line
- Huge pages
- Linux 7.0 cuts PostgreSQL throughput in half
- Linux Kernel Startup
- Linux preemption models
- PostgreSQL