В информатике сравнение и обмен ( CAS ) — это атомарная инструкция , используемая в многопоточности для достижения синхронизации . Она сравнивает содержимое ячейки памяти с заданным значением и , только если они совпадают, изменяет содержимое этой ячейки памяти на новое заданное значение. Это делается как одна атомарная операция. Атомарность гарантирует, что новое значение вычисляется на основе актуальной информации; если бы значение было обновлено другим потоком в это время, запись бы не удалась. Результат операции должен указывать, выполнила ли она замену; это можно сделать либо с помощью простого логического ответа (этот вариант часто называют сравнением и установкой ), либо путем возврата значения, считанного из ячейки памяти ( а не значения, записанного в нее).
Операция сравнения и обмена представляет собой атомарную версию следующего псевдокода , где * обозначает доступ через указатель : [1]
функция cas(p: указатель на int, old: int, new: int) — если *p ≠ old, возвращает false *р ← новый вернуть истину
Эта операция используется для реализации примитивов синхронизации , таких как семафоры и мьютексы , [1] , а также более сложных алгоритмов без блокировки и ожидания . Морис Херлихи (1991) доказал, что CAS может реализовать больше этих алгоритмов, чем атомарное чтение, запись или выборка и добавление , и, предполагая довольно большой [ требуется разъяснение ] объем памяти, что он может реализовать их все. [2] CAS эквивалентен load-link/store-conditional в том смысле, что постоянное число вызовов любого примитива может использоваться для реализации другого без ожидания . [3]
Алгоритмы, построенные вокруг CAS, обычно считывают некоторую ключевую ячейку памяти и запоминают старое значение. На основе этого старого значения они вычисляют некоторое новое значение. Затем они пытаются подставить новое значение с помощью CAS, где сравнение проверяет, что ячейка все еще равна старой. Если CAS указывает, что попытка не удалась, ее нужно повторить с самого начала: ячейка перечитывается, новое значение пересчитывается и CAS пробуется снова. Исследователи обнаружили, что вместо немедленного повтора после сбоя операции CAS общая производительность системы может быть улучшена в многопроцессорных системах, где многие потоки постоянно обновляют некоторую определенную общую переменную, если потоки, которые видят, что их CAS терпит неудачу, используют экспоненциальную задержку — другими словами, немного подождать перед повторной попыткой CAS. [4]
В качестве примера использования compare-and-swap, вот алгоритм для атомарного увеличения или уменьшения целого числа . Это полезно в различных приложениях, использующих счетчики. Функция add выполняет действие *p ← *p + a атомарно (снова обозначая косвенность указателя как * , как в C) и возвращает конечное значение, сохраненное в счетчике. В отличие от псевдокода cas выше, нет требования, чтобы любая последовательность операций была атомарной, за исключением cas .
функция add(p: указатель на int, a: int) возвращает int сделано ← ложно пока не сделано значение ← *p // Даже эта операция не обязательно должна быть атомарной. сделано ← cas(p, значение, значение + a) возвращаемое значение + а
В этом алгоритме, если значение *p изменится после (или во время!) его извлечения и до того, как CAS выполнит сохранение, CAS заметит и сообщит об этом факте, заставив алгоритм повторить попытку. [5]
Некоторые алгоритмы на основе CAS подвержены и должны обрабатывать проблему ложного положительного совпадения или проблему ABA . Возможно, что между временем считывания старого значения и временем попытки CAS некоторые другие процессоры или потоки изменяют расположение памяти два или более раз, так что оно приобретает битовую комбинацию, которая соответствует старому значению. Проблема возникает, если эта новая битовая комбинация, которая выглядит точно так же, как старое значение, имеет другое значение: например, это может быть переработанный адрес или обернутый счетчик версий.
Общим решением этой проблемы является использование CAS двойной длины (DCAS). Например, в 32-битной системе можно использовать 64-битный CAS. Вторая половина используется для хранения счетчика. Часть сравнения операции сравнивает ранее считанное значение указателя и счетчика с текущим указателем и счетчиком. Если они совпадают, происходит обмен — записывается новое значение — но новое значение имеет увеличенный счетчик. Это означает, что если произошел ABA, хотя значение указателя будет тем же самым, счетчик вряд ли будет тем же самым (для 32-битного значения должно было бы произойти кратное 2 32 операций, что привело бы к переносу счетчика, и в этот момент значение указателя также должно было бы случайно быть тем же самым).
Альтернативная форма этого (полезная для ЦП, не имеющих DCAS) — использовать индекс в свободном списке, а не полный указатель, например, с 32-битным CAS используйте 16-битный индекс и 16-битный счетчик. Однако уменьшенная длина счетчика начинает делать ABA возможным на современных скоростях ЦП.
Одним из простых методов, помогающих решить эту проблему, является хранение счетчика ABA в каждом элементе структуры данных вместо использования одного счетчика ABA для всей структуры данных.
Более сложным, но и более эффективным решением является реализация безопасного высвобождения памяти (SMR). По сути, это сборка мусора без блокировок. Преимущество использования SMR заключается в гарантии того, что данный указатель будет существовать только один раз в любой момент времени в структуре данных, таким образом, проблема ABA полностью решена. (Без SMR будет использоваться что-то вроде списка свободных адресов, чтобы гарантировать, что ко всем элементам данных можно получить безопасный доступ (без нарушений доступа к памяти), даже если они больше не присутствуют в структуре данных. С SMR будет осуществляться доступ только к элементам, которые в данный момент находятся в структуре данных).
CAS и другие атомарные инструкции иногда считаются ненужными в однопроцессорных системах, поскольку атомарность любой последовательности инструкций может быть достигнута путем отключения прерываний во время ее выполнения. Однако отключение прерываний имеет множество недостатков. Например, код, которому разрешено это делать, должен быть надежным, чтобы не быть вредоносным и не монополизировать ЦП, а также быть правильным и не повесить машину случайно в бесконечном цикле или ошибке страницы. Кроме того, отключение прерываний часто считается слишком дорогим, чтобы быть практичным. Таким образом, даже программы, предназначенные только для работы на однопроцессорных машинах, выиграют от атомарных инструкций, как в случае фьютексов Linux .
В многопроцессорных системах обычно невозможно отключить прерывания на всех процессорах одновременно. Даже если бы это было возможно, два или более процессоров могли бы попытаться получить доступ к памяти одного и того же семафора одновременно, и, таким образом, атомарность не была бы достигнута. Инструкция сравнения и обмена позволяет любому процессору атомарно проверять и изменять область памяти, предотвращая такие многопроцессорные коллизии.
На многопроцессорных архитектурах серверного уровня 2010-х годов сравнение и подстановка обходятся дешевле простой загрузки, которая не обслуживается из кэша. В статье 2013 года указывается, что CAS всего в 1,15 раза дороже загрузки без кэширования на Intel Xeon ( Westmere-EX ) и в 1,35 раза на AMD Opteron ( Magny-Cours ). [6]
Compare-and-swap (и compare-and-swap-double) были неотъемлемой частью архитектур IBM 370 (и всех последующих) с 1970 года. Операционные системы, работающие на этих архитектурах, широко используют эту инструкцию для облегчения параллелизма процессов (т. е. системных и пользовательских задач) и процессоров (т. е. центральных процессоров) , устраняя, в максимально возможной степени, «отключенные спин-блокировки », которые использовались в более ранних операционных системах IBM. Аналогичным образом, использование test-and-set также было устранено. В этих операционных системах новые единицы работы могут быть инстанциированы «глобально», в глобальный список приоритетов служб, или «локально», в локальный список приоритетов служб, путем выполнения одной инструкции compare-and-swap. Это существенно улучшило отзывчивость этих операционных систем.
В архитектурах x86 (начиная с 80486 ) и Itanium это реализовано как инструкция сравнения и обмена ( CMPXCHG ) (на многопроцессорной системе необходимо использовать префикс LOCK ).
По состоянию на 2013 год большинство многопроцессорных архитектур поддерживают CAS на аппаратном уровне, а операция сравнения и обмена является наиболее популярным примитивом синхронизации для реализации как блокирующих, так и неблокирующих параллельных структур данных . [4]
Операции атомарного счетчика и атомарной битовой маски в ядре Linux обычно используют инструкцию сравнения и обмена в своей реализации. Архитектуры SPARC-V8 и PA-RISC являются двумя из немногих недавних архитектур, которые не поддерживают CAS на аппаратном уровне; порт Linux на эти архитектуры использует спин-блокировку . [7]
Многие компиляторы C поддерживают использование функции сравнения и обмена либо с помощью функций C11 [8] , либо с помощью некоторого нестандартного расширения C данного компилятора C [9] , либо путем вызова функции, написанной непосредственно на языке ассемблера, с использованием инструкции сравнения и обмена.<stdatomic.h>
Следующая функция C демонстрирует базовое поведение варианта сравнения и обмена, который возвращает старое значение указанной области памяти; однако эта версия не обеспечивает критически важных гарантий атомарности, которые могла бы предоставить настоящая операция сравнения и обмена:
int compare_and_swap ( int * reg , int oldval , int newval ) { ATOMIC (); int old_reg_val = * reg ; if ( old_reg_val == oldval ) * reg = newval ; END_ATOMIC (); return old_reg_val ; }
old_reg_val
всегда возвращается, но его можно проверить после compare_and_swap
операции, чтобы увидеть, соответствует ли он oldval
, так как он может отличаться, что означает, что другому процессу удалось успешно compare_and_swap
изменить значение reg с oldval
.
Например, протокол выборов может быть реализован таким образом, что каждый процесс проверяет результат compare_and_swap
по своему собственному PID (= newval). Победивший процесс находит compare_and_swap
возвращающее начальное значение, отличное от PID (например, ноль). Для проигравших он вернет победивший PID.
Вот логика в руководстве по программному обеспечению Intel, том 2A:
bool Compare_and_swap ( int * accum , int * dest , int newval ) { if ( * accum == * dest ) { * dest = newval ; вернуть истину ; } Еще { * аккум = * место ; вернуть ложь ; } }
Поскольку CAS работает с одной ячейкой памяти размером с указатель , в то время как большинству алгоритмов без блокировок и без ожидания необходимо изменять несколько ячеек, было реализовано несколько расширений.
java.util.concurrent.atomic
реализует «compareAndSet» в различных классах