Является ли volatile правильным способом создания одиночного байтового атома в C/С++?

Я знаю, что volatile не применяет атомарность для int, например, но делает ли он доступ к одному байту? Семантика требует, чтобы записи и чтения всегда были из памяти, если я правильно помню.

Или, другими словами: читают ли и обрабатывают ли байты всегда атомарно?

Ответ 1

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

Процессоры обычно читают и записывают одиночные байты атомарно. Проблема возникает из-за того, что, когда у вас несколько ядер, не все ядра будут видеть байт как написанный одновременно. Фактически, это может быть довольно долго (в CPU говорят, тысячи или миллионы инструкций (ака, микросекунды или, возможно, миллисекунды)), прежде чем все ядра увидели запись.

Итак, вам нужны несколько неназванные атомные операции С++ 0x. Они используют инструкции CPU, которые гарантируют, что порядок вещей не перепутался, и что, когда другие ядра смотрят на значение, которое вы написали после того, как вы его написали, они видят новое значение, а не старое. Их работа заключается не столько в атомарности операций, сколько в том, чтобы обеспечить соответствующие шаги синхронизации.

Ответ 2

В стандарте ничего не говорится об атомарности.

Ответ 3

Ключевое слово volatile используется для указания того, что переменной (int, char или иначе) может быть присвоено значение из внешнего непредсказуемого источника. Это обычно препятствует компилятору оптимизировать переменную.

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

Ответ 4

Короткий ответ. Не используйте volatile для обеспечения атомарности.

Длинный ответ Можно подумать, что, поскольку процессоры обрабатывают слова в одной команде, простые операции с текстом по сути являются потокобезопасными. Идея использования volatile заключается в том, чтобы затем гарантировать, что компилятор не делает предположений о значении, содержащемся в общей переменной.

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

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

Как правило, они предотвращают чтение и запись из строя, вызывая команду "Барьер памяти памяти" (DMB on ARM), которая гарантирует, что чтение и запись происходят в правильном порядке. Посмотрите здесь для более подробной информации.

Другая проблема с volatile заключается в том, что она не позволит компилятору делать оптимизацию, даже если это нормально.

Ответ 5

На любом нормальном процессоре чтение и запись любого выровненного, размерного или меньшего по размеру типа является атомарным. Это не проблема. Проблемы:

  • Просто потому, что чтение и запись являются атомарными, из этого не следует, что последовательности чтения/изменения/записи являются атомарными. На языке C x++ концептуально является циклом чтения/изменения/записи. Вы не можете контролировать, генерирует ли компилятор атомный приращение, и вообще не будет.
  • Проблемы синхронизации кеша. На архитектуре с половинным дерьмом (почти ничего, кроме x86) аппаратное обеспечение слишком тупые, чтобы гарантировать, что представление памяти, которое каждый процессор видит, отражает порядок, в котором происходит запись. Например, если cpu 0 записывает адреса A и B, возможно, что cpu 1 видит обновление по адресу B, но не обновление по адресу A. Для решения этой проблемы нужны специальные коды памяти/барьера памяти, а компилятор не будет генерировать они для вас.

Второй момент имеет значение только для SMP/многоядерных систем, поэтому, если вы счастливы ограничить себя одноядерным, вы можете его игнорировать, а затем простые чтения и записи будут атомарными в C на любой разумной архитектуре процессора. Но вы не можете сделать много полезного именно с этим. (Например, единственный способ реализовать простую блокировку таким образом включает в себя пространство O(n), где n - количество потоков.)