Как функция compare_exchange C ++ определяет условия гонки?

Как мы знаем, compare_exchange_weak() возвращает ошибку (ложное значение), если есть условие гонки, поэтому операция не может быть выполнена полностью. Но как именно состояние гонки определяется compare_exchange_weak() ?

Выполняет ли инструкция lock cmpxchg ошибку, если больше того, что один поток пытается прочитать / записать значение, т.е. получить блокировку и именно таким образом compare_exchange_weak определяет состояние гонки?

Всего 2 ответа


Команда cmpxchg влияет на флаг ZF : она установлена, если обмен выполнен успешно, и он очищается в противном случае.

Давайте посмотрим на это с примером:

std::atomic<int> a;

bool my_compare_exchange(int expected, int desired) {
   bool succeeded = a.compare_exchange_weak(expected, desired);
   return succeeded;
}

Функция my_compare_exchange() преобразуется в следующий код сборки:

my_compare_exchange:
        mov     eax, edi
        lock cmpxchg    DWORD PTR a[rip], esi
        sete    al // <-- conditional instruction
        ret

Регистр al устанавливается sete al 1 с использованием sete al если обмен сменился (т. cmpxchg ZF был установлен на cmpxchg ). В противном случае он устанавливается на ноль (т. cmpxchg ZF очищается на cmpxchg ).


lock cmpxchg является атомной; во время исполнения ничего не может быть (логически в любом случае). В отличие от реализации compare_exchange_weak LL / SC, он не может compare_exchange_weak терпеть неудачу, когда другой поток записывается внутри одной и той же строки кэша, только если сравнение фактически не выполняется. ( Может ли CAS сбой для всех потоков? )

compare_exchange_strong может быть реализован как lock cmpxchg не требуя цикла.


Вы спрашиваете о прецеденте, когда вы загружаете старое значение, изменяете его, а затем пытаетесь использовать CAS новое значение? (например, для синтеза атомарной операции, которую аппаратное обеспечение не предоставляет напрямую, например, атомный FP-добавление или атомный явный-младший бит ).

В этом случае да, причиной отказа cmpxchg будет гонка с другим потоком.

Как объяснил @ ネ ロ ク, вы проверяете результат флага cmpxchg чтобы получить логический результат compare_exchange_strong , то есть указать, не сработала ли эта часть сравнения, и было ли хранилище выполнено или нет. См . Руководство пользователя Intel для cmpxchg .


Существуют и другие прецеденты для lock cmpxchg , например, если ждать, когда спин-блокировка станет доступной, вращаясь на lock.compare_exchange_weak(0, 1) , многократно пытаясь разблокировать переменную CAS в заблокированное состояние. Таким образом, неудачный CAS не из состояния гонки, это просто из замка, все еще удерживаемого. (Потому что вы передаете константу как «ожидаемую», а не то, что вы только что прочитали из этого места.)

Это, как правило, не очень хорошая идея, AFAIK. Я думаю, что обычно лучше прятать только чтение, ожидая блокировки, и только попытайтесь взять блокировку с CAS или XCHG, если увидите, что она доступна. (Использование xchg и тестирование целочисленного результата потенциально дешевле, чем lock cmpxchg , возможно, сохранение строки кэша за меньшее количество циклов. Блокирует манипуляцию с памятью через встроенную сборку )


TL: DR: беззаботное программирование требует точного языка, если вы будете рассуждать о правильности.

cmpxchg терпит неудачу и очищает ZF (создавая условие не равного), если EAX не соответствует значению в памяти при его выполнении. Сама инструкция не знает или не заботится о гонках, просто точное состояние, когда оно само выполняет.

Вы, как программист, должны беспокоиться о гонках и повторять попытки использовать его в качестве строительного блока для создания больших атомных операций, построенных поверх него.


Есть идеи?

10000