Test-and-set
Test-and-set (TAS) — это атомарная машинная инструкция, используемая в многопроцессорных системах для реализации примитивов синхронизации, таких как мьютексы (mutex) и семафоры. Операция выполняет чтение значения из ячейки памяти и запись в неё нового значения (обычно единицы) как единое неделимое действие, что предотвращает состояние гонки (race condition) при одновременном доступе нескольких потоков или процессов к общему ресурсу.
Принцип работы
Инструкция test-and-set работает с одним операндом — адресом ячейки памяти (обычно размером в слово). Её выполнение состоит из двух шагов, которые гарантированно выполняются атомарно (без прерывания):
- Чтение текущего значения из указанной ячейки памяти.
- Запись в эту же ячейку нового значения (как правило, 1 или
true).
Результатом операции является старое значение, прочитанное на первом шаге. Если старое значение равно 0 (свободно), поток считается захватившим ресурс; если 1 (занято), поток понимает, что ресурс уже используется другим потоком, и может либо повторить попытку (спин-блокировка), либо перейти в режим ожидания.
Псевдокод
Атомарная операция test-and-set может быть описана следующим псевдокодом:
`` function test_and_set(lock) { old = lock; // Чтение текущего значения lock = 1; // Запись единицы return old; // Возврат старого значения } ``
В реальных процессорах эта последовательность выполняется аппаратно за один такт или за несколько тактов, но без возможности прерывания другими потоками или ядрами.
История
Концепция атомарных операций для синхронизации возникла в 1960-х годах с развитием многозадачных операционных систем. Первые реализации test-and-set появились в мейнфреймах IBM System/360 (1964 год), где использовалась инструкция CS (Compare and Swap) и её модификации. В 1970-х годах алгоритм test-and-set был формализован в контексте теории параллельных вычислений, в частности, в работах Эдсгера Дейкстры по взаимному исключению.
В 1980-х годах, с распространением многопроцессорных систем (например, Symmetry SGI, Sequent Balance), test-and-set стала стандартной инструкцией в наборах команд процессоров Intel x86 (инструкция LOCK XCHG), ARM (инструкции LDREX/STREX), SPARC и других архитектур. В современных процессорах test-and-set часто реализуется через более гибкие примитивы, такие как compare-and-swap (CAS), но остаётся базовым элементом для построения низкоуровневых блокировок.
Реализация в процессорах
Архитектура x86
В процессорах Intel и AMD инструкция test-and-set реализуется через префикс LOCK в сочетании с инструкцией XCHG (обмен значениями). Например:
``assembly lock xchg eax, [lock_var] ; Атомарно обменивает eax (обычно 1) с содержимым lock_var ``
Префикс LOCK блокирует шину памяти или кэш-линию, гарантируя атомарность операции на уровне аппаратуры. В многоядерных системах используется протокол когерентности кэша (MESI), чтобы избежать блокировки всей шины.
Архитектура ARM
В процессорах ARM (включая Cortex-A, Apple Silicon) test-and-set реализуется через пару инструкций LDREX (Load-Exclusive) и STREX (Store-Exclusive). LDREX читает значение и помечает ячейку памяти как «отслеживаемую», а STREX пытается записать новое значение, проверяя, не изменилась ли ячейка другим ядром. Если запись прошла успешно, операция считается атомарной. Пример:
``assembly try: ldrex r0, [r1] ; Атомарное чтение cmp r0, #0 ; Проверка, свободен ли ресурс strexeq r2, #1, [r1] ; Попытка записи единицы cmpeq r2, #0 ; Проверка успешности записи bne try ; Если не удалось — повторить ``
Архитектура RISC-V
В RISC-V (открытый стандарт) test-and-set реализуется через инструкции атомарного доступа (A-расширение), например AMOSWAP.W (Atomic Memory Swap Word):
``assembly amoswap.w r0, r1, (r2) ; Атомарно обменивает r1 с содержимым по адресу r2, результат в r0 ``
Применение
Реализация мьютексов
Test-and-set используется для построения простейших спин-блокировок (spinlock). Поток циклически вызывает TAS, пока не получит значение 0 (свободно). Пример на C:
```c volatile int lock = 0;
void acquire() { while (test_and_set(&lock) == 1) { // ожидание (spinning) } }
void release() { lock = 0; // не обязательно атомарно, так как только один поток пишет } ```
Семафоры и барьеры
На основе TAS строятся более сложные примитивы синхронизации: семафоры (счётчики ресурсов), барьеры памяти (memory barriers), условные переменные. В операционных системах (Linux, Windows) test-and-set используется в ядре для быстрой синхронизации коротких критических секций.
Очереди без блокировок (lock-free)
Test-and-set применяется в алгоритмах без блокировок, таких как lock-free стек (Treiber stack) или lock-free очередь (Michael-Scott). В этих алгоритмах TAS используется для атомарного обновления указателей.
Недостатки и ограничения
Проблема «активного ожидания» (busy-waiting)
Основной недостаток test-and-set в спин-блокировках — трата процессорного времени на циклы ожидания, особенно при высокой конкуренции. Это может привести к снижению производительности и перегреву процессора. Для смягчения применяются методы:
- Пауза (pause instruction на x86) — снижает энергопотребление.
- Экспоненциальная задержка (exponential backoff) — поток ждёт всё дольше между попытками.
- Передача управления (yield) — поток добровольно отдаёт процессорное время.
Проблема «инверсии приоритетов»
В системах с приоритетами потоков низкоприоритетный поток, захвативший блокировку через TAS, может быть вытеснен высокоприоритетным, который будет ждать освобождения ресурса. Это решается через протоколы наследования приоритетов (priority inheritance) в операционных системах реального времени.
Ограничение на масштабируемость
При большом числе ядер (16 и более) спин-блокировки на основе TAS могут вызывать перегрузку шины памяти из-за постоянных попыток записи. Для таких случаев применяются более эффективные алгоритмы: MCS-блокировка (Mellor-Crummey and Scott) или ticket-блокировка, которые используют очереди ожидания на уровне ядра.
Альтернативы
- Compare-and-swap (CAS): более гибкая атомарная операция, позволяющая сравнивать и менять значение только при совпадении ожидаемого. Используется в lock-free структурах данных.
- Load-link/store-conditional (LL/SC): пара инструкций, как в ARM (LDREX/STREX), позволяющая реализовать CAS и TAS без блокировки шины.
- Fetch-and-add (FAA): атомарное прибавление константы к ячейке памяти, используется в счётчиках и барьерах.
- Test-and-test-and-set (TTAS): модификация TAS, где поток сначала читает значение без записи (обычным
load), и только если оно равно 0, пытается выполнить TAS. Это снижает нагрузку на шину.
Примеры в языках программирования
- C++11:
std::atomic_flag— реализует test-and-set через методtest_and_set(). - Java:
AtomicBoolean— методcompareAndSet()(основан на CAS), но для TAS используетсяgetAndSet(true). - C#:
Interlocked.Exchange(ref lock, 1)— атомарный обмен, аналогичный TAS. - Linux kernel:
atomic_xchg()— функция для атомарного обмена, эквивалентная TAS.
Интересные факты
- Инструкция test-and-set в процессорах x86 (через
LOCK XCHG) является одной из самых старых атомарных операций, сохранившихся с 1980-х годов. - В суперкомпьютерах Cray (1980-е) test-and-set реализовывался на уровне аппаратных регистров, что позволяло синхронизировать до 64 процессоров.
- В некоторых архитектурах (например, в ранних версиях ARM) test-and-set отсутствовал аппаратно, и его эмулировали через прерывания и отключение кэша.
Источники
- Maurice Herlihy, Nir Shavit. The Art of Multiprocessor Programming. Morgan Kaufmann, 2012.
- Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A: Instruction Set Reference, A-L.
- ARM Architecture Reference Manual, ARMv8-A.
- Andrew S. Tanenbaum, Herbert Bos. Modern Operating Systems. Pearson, 2015.
- Документация Linux kernel:
Documentation/locking/spinlocks.txt.
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →