Если волатильность бесполезна для потоковой передачи, почему атомарные операции требуют указателей на изменчивые данные?

Я читал из многих источников, что ключевое слово volatile не помогает в многопоточных сценариях. Однако это утверждение постоянно оспаривается атомными функциями работы, которые принимают указатели volatile.

Например, в Mac OS X мы имеем семейство функций OSAtomic:

SInt32 OSIncrementAtomic(volatile SInt32 *address);
SInt32 OSDrecrementAtomic(volatile SInt32 *address);
SInt32 OSAddAtomic(SInt32 amount, volatile SInt32 *address);
// ...

И похоже, что аналогичное использование ключевого слова volatile в Windows для операций Interlocked:

LONG __cdecl InterlockedIncrement(__inout LONG volatile *Addend);
LONG __cdecl InterlockedDecrement(__inout LONG volatile *Addend);

Также кажется, что в С++ 11 атомные типы имеют методы с модификатором volatile, что должно как-то означать, что ключевое слово volatile имеет какую-то связь с атомарностью.

Итак, что мне не хватает? Почему производители ОС и разработчики стандартных библиотек настаивают на использовании ключевого слова volatile для целей потоковой передачи, если это не полезно?

Ответ 1

Внезапно мне пришло в голову, что я просто неверно истолковал смысл volatile*. Подобно const* означает, что точка не должна меняться, volatile* означает, что указывающий не должен кэшироваться в регистре. Это дополнительное ограничение, которое можно свободно добавлять: столько, сколько вы можете отбрасывать char* в const char*, вы можете применить int* к volatile int*.

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

Ответ 2

Volatile не бесполезен для совместного доступа несколькими потоками - он просто не обязательно достаточен:

  • он не обязательно обеспечивает семантику барьера памяти, которая может потребоваться;
  • он не обеспечивает гарантии атомного доступа (например, если изменчивый объект я s больше размера слова нативной памяти платформы)

Кроме того, следует также отметить, что квалификатор volatile в аргументах указателя API в вашем примере действительно только действительно добавляет возможность API-интерфейсам получать указатели на объекты volatile без жалобы - это не требует что указатели указывают на фактические объекты volatile. Стандарт позволяет неквалифицированному указателю автоматически преобразовываться в квалифицированный указатель. Автоматический переход в другую сторону (квалифицированный указатель на неквалифицированный) не предусмотрен в стандарте (компиляторы обычно позволяют это, но выдают предупреждение).

Например, если InterlockedIncrement() были прототипированы как:

LONG __cdecl InterlockedIncrement(__inout LONG *Addend);  // not `volatile*`

API все еще может быть реализован для правильной работы внутри компании. Тем не менее, если у пользователя был volatile obeject, который он хотел передать API, для отклика компилятора потребуется бросить вызов.

Так как (необходимо или нет), эти API часто используются с volatile квалифицированными объектами, добавив квалификатор volatile к аргументу указателя, который предотвращает создание бесполезной диагностики при использовании API и ничего не вредит, когда API используется с указателем на энергонезависимый объект.

Ответ 3

С++ 11 имеет атомы для переменных volatile и non volatile.

Если встроенные средства компилятора принимают указатель на volatile int, это означает, что вы можете использовать его , даже если переменная является изменчивой. Это не мешает вам использовать функцию для данных не volatile.

Ответ 4

Ну, ключевое слово "volatile" гарантирует, что компилятор всегда загружает/сохраняет значение переменной из/в память каждый раз, когда переменная отображается в вашем коде.
Это предотвращает определенные оптимизации, например. что значение просто загружается в регистр один раз, а затем используется несколько раз.
Это полезно, когда у вас есть несколько потоков, которые могут изменять "общие" переменные между потоками. Вам нужно будет всегда загружать/сохранять значение из/в память, чтобы проверить его значение, которое может быть изменено другим потоком. Если volatile не использовался, другой поток, возможно, не записал новое значение в память (но поместил его в регистр или, возможно, произошла какая-либо другая оптимизация), и первый поток не заметил бы никакого изменения стоимости.

В ваших случаях "volatile SInt32 * address" сообщает компилятору, что память, указанная по адресу, может быть изменена любым источником. Отсюда и необходимость атомной операции.