Как реализован std::atomic

Я изучаю разницу между mutex и atomic в С++ 11.

Насколько я понимаю, mutex — это своего рода механизм блокировки, который реализован на базе ОС/ядра. Например, Linux предлагает механизм futex. С помощью futex мы могли реализовать mutex и semaphore. Кроме того, я знаю, что futex реализуется низкоуровневой атомарной операцией, такой как CompareAndSet, CompareAndSwap.

Что касается std::atomic, я знаю, что он реализован на основе модели памяти, представленной в C++11. Однако я не знаю, как реализована модель памяти на низком уровне. Если это также реализуется атомарной операцией, такой как CompareAndSet, в чем разница между std::atomic и mutex?

Одним словом, если std::atomic::is_lock_free дает мне false, ну, я скажу, что std::atomic это одно и то же с mutex. Но если он дает мне true, как это реализовано на низком уровне?


person Yves    schedule 04.12.2019    source источник
comment
Может ли num++ быть атомарным для 'int num'? объясняет, как x86 asm/hardware реализует атомарные операции, в частности операции RMW, со ссылками на другие материалы. Атомарные операции — это строительные блоки, из которых может быть построен мьютекс. Также Atomicity на x86 и Почему целочисленное назначение для естественно выровненной переменной атомарно на x86?.   -  person Peter Cordes    schedule 04.12.2019
comment
Связано: preshing.com/20120930/weak-vs-strong-memory-models говорит о некоторых различных моделях аппаратной памяти, где вам нужно больше или меньше барьеров для реализации acq_rel. Другие статьи Джеффа Прешинга тоже очень хороши для чтения. Кстати, этот вопрос может быть слишком широким; Я мог бы просто опубликовать asm для того, как некоторые операторы C++ компилируются в различных ISA (или вы можете посмотреть сами на godbolt.org с оптимизация включена), но без объяснения моделей памяти HW для этих ISA IDK, если это ответит на ваш вопрос. На самом деле понимание безблокировочного кода требует объема информации на уровне книги.   -  person Peter Cordes    schedule 04.12.2019
comment
Прочитайте stackoverflow.com/questions/6319146/ (это почти дубликат, но включает много дополнительной информации).   -  person Richard Critten    schedule 04.12.2019
comment
@PeterCordes Большое спасибо за объяснение, ну а если std::atomic реализовано по разному на разных архитектурах, то думаю мне достаточно одного примера на какой-то конкретной архитектуре, например X86.   -  person Yves    schedule 05.12.2019


Ответы (2)


Если атомарные операции lock_free, они, вероятно, реализованы так же, как реализованы компоненты мьютекса. В конце концов, чтобы заблокировать мьютекс, вам действительно нужна какая-то атомарная операция, гарантирующая, что один и только один поток заблокирует мьютекс.

Разница в том, что атомарные операции, которые не блокируются, не имеют «заблокированного» состояния. Давайте сравним два возможных способа атомарного приращения переменной:

Во-первых, мьютексный способ. Мы блокируем мьютекс, читаем-увеличиваем-записываем переменную, затем разблокируем мьютекс. Если поток будет прерван во время чтения-инкремента-записи, другие потоки, пытающиеся выполнить ту же операцию, будут заблокированы, пытаясь заблокировать мьютекс. (См. Где находится блокировка для std::atomic? для как это работает в некоторых реальных реализациях, для объектов слишком больших, чтобы быть lock_free.)

Во-вторых, атомный способ. ЦП «блокирует» только строку кэша, содержащую переменную, которую мы хотим изменить, на время выполнения одной инструкции чтения-увеличения-записи. (Это означает, что ЦП задерживает ответ на запросы MESI о признании недействительной или совместном использовании строки кэша, сохраняя эксклюзивный доступ, чтобы ни один другой ЦП не мог просмотреть ее. Когерентность кэша MESI всегда требует исключительного владения строкой кэша, прежде чем ядро ​​сможет изменить ее, поэтому это дешево, если мы уже владели линией). Мы не можем быть прерваны во время инструкции. Другой поток, пытающийся получить доступ к этой переменной, в худшем случае должен ждать, пока аппаратное обеспечение когерентности кеша определит, кто может изменять расположение памяти.

Так как же нам заблокировать мьютекс? Вероятно, мы выполняем атомарное сравнение и обмен. Таким образом, легкие атомарные операции — это примитивы, из которых собираются тяжелые операции мьютексов.

Конечно, это все зависит от платформы. Но это то, что делают типичные современные платформы, которые вы, вероятно, будете использовать.

person David Schwartz    schedule 05.12.2019
comment
Я добавил к этому несколько пояснений, особенно о блокировках кеша, которые используют то же слово, что и программная блокировка мьютекса/спин-блокировки, но очень отличаются. Связано: Может ли num++ быть атомарным для 'int num'?. Также, чтобы расширить это: обратите внимание, что машины LL/SC (почти все основные не-x86 ISA) используют условие сохранения, которое проверяет, сохраняем ли мы исключительное право собственности, вместо того, чтобы с самого начала брать блокировку кеша. Сокращение наихудшей задержки для других ядер за счет потери пропускной способности из-за повторных попыток. - person Peter Cordes; 05.12.2019
comment
Кроме того, здесь ничего не говорится о том, как std::atomic<T> обеспечивает последовательную согласованность (или, возможно, более слабое упорядочение); это важная часть того, что он делает для чистой загрузки и чистого хранения. (И RMW на не-x86) - person Peter Cordes; 05.12.2019
comment
(Но я думаю, что отчасти это вина слишком широкого вопроса.) - person Peter Cordes; 05.12.2019

в чем разница между std::atomic и мьютексом

Мьютекс — это конструкция параллелизма, независимая от каких-либо пользовательских данных, предлагающая lock и unlock методы, позволяющие защитить (взаимное исключение внутри) области кода. Вы можете поместить все, что хотите, в этот регион.

std::atomic<T> – это адаптер для одного экземпляра типа T, обеспечивающий атомарный доступ для каждой операции к этому объекту.

Мьютекс является более общим в том смысле, что одной из возможных реализаций std::atomic является защита любого доступа к нижележащему объекту с помощью мьютекса.

std::atomic существует в основном из-за другой распространенной реализации: использования атомарной инструкции2 для непосредственного выполнения операции без использования мьютекса. Это реализация, используемая, когда std::atomic<T>::is_lock_free() возвращает true. Как правило, это более эффективно, чем подход с мьютексом, но применим только к объектам, достаточно маленьким, чтобы ими можно было манипулировать «одним выстрелом» с помощью атомарных инструкций.


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

Например, на x86 компиляторы реализуют все загрузки std::atomic для достаточно малых значений с простыми загрузками и реализовать все хранилища слабее, чем memory_order_seq_cst, с обычными хранилищами. Однако seq_cst хранилища реализованы со специальными инструкциями - завершающий mfence после mov в GCC до 10.1 и (неявный lock) xchg mem,reg в clang, недавнем GCC и других компиляторах.

Обратите также внимание на то, что асимметрия между загрузками и хранилищами является выбором компилятора: вместо этого они могли бы поместить специальную обработку в seq_cst загрузок, но поскольку загрузки обычно превосходят количество хранилищ, это в большинстве случаев медленнее. (И потому, что дешевые загрузки на быстрых путях более ценны.)

person BeeOnRope    schedule 05.12.2019
comment
Я думаю, что ваша сноска в лучшем случае сбивает с толку. Если операция не является атомарной по отношению к POV других потоков, вы не можете использовать ее для реализации std::atomic без блокировки. например ARM может использовать ldp для загрузки выровненной пары регистров в качестве реализации для atomic<uint64>::load(memory_order_relaxed) из-за документированных гарантий того, что это атомарно для выровненных загрузок на некоторых микроархитектурах. Это не специальная инструкция, которая существует только из-за своей атомарности, если вы это имели в виду. Или, если вы имеете в виду x86 add [mem], reg без префикса lock на однопроцессорном процессоре, это атомарно. переключатель контекста - person Peter Cordes; 05.12.2019
comment
@PeterCordes - возможно, неатомарность - не лучшая терминология. Я просто имею в виду простые операции, такие как, скажем, загрузка и сохранение, которые иногда реализуют достаточную атомарность для реализации некоторых методов std::atomic (по крайней мере, для некоторых порядков памяти), не являясь при этом специальной медленной атомарной операцией на уровне ISA. - person BeeOnRope; 05.12.2019