Если волатильность бесполезна для потоковой передачи, почему атомарные операции требуют указателей на изменчивые данные?
Я читал из многих источников, что ключевое слово 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" сообщает компилятору, что память, указанная по адресу, может быть изменена любым источником. Отсюда и необходимость атомной операции.