Являются ли функции блокировки мьютекса достаточными без летучих?
Сотрудник и я пишу программное обеспечение для различных платформ, работающих на x86, x64, Itanium, PowerPC и других 10-летних серверных CPU.
Мы просто обсудили, достаточно ли функции мьютекса, такие как pthread_mutex_lock()... pthread_mutex_unlock(), или же должна быть уязвимой защищенная переменная.
int foo::bar()
{
//...
//code which may or may not access _protected.
pthread_mutex_lock(m);
int ret = _protected;
pthread_mutex_unlock(m);
return ret;
}
Моя забота - это кеширование. Может ли компилятор разместить копию _ защищенного в стеке или в регистре и использовать это устаревшее значение в присваивании? Если нет, что мешает этому? Являются ли вариации этого шаблона уязвимыми?
Я предполагаю, что компилятор фактически не понимает, что pthread_mutex_lock() является специальной функцией, поэтому мы просто защищены точками последовательности?
Большое спасибо.
Обновление: Хорошо, я вижу тенденцию с ответами, объясняющими, почему волатильность плоха. Я уважаю эти ответы, но статьи по этому вопросу легко найти в Интернете. То, что я не могу найти в Интернете, и причина, по которой я задаю этот вопрос, заключается в том, как я защищен без изменчивости. Если приведенный выше код верен, как он неуязвим для проблем кеширования?
Ответы
Ответ 1
Если приведенный выше код верен, как он неуязвим для кэширования вопросы?
До С++ 0x это не так. И это не указано в C. Таким образом, это действительно зависит от компилятора. В общем случае, если компилятор не гарантирует, что он будет соблюдать ограничения порядка доступа к памяти для функций или операций, которые связаны с несколькими потоками, вы не сможете написать многопоточный безопасный код с этим компилятором. См. Hans J Boehm Нити не могут быть реализованы как библиотека.
Что касается абстракций, которые ваш компилятор должен поддерживать для безопасного потока, то запись в wikipedia на Memory Barriers является довольно хорошей отправной точкой.
(Что касается того, почему люди предложили volatile
, некоторые компиляторы рассматривают volatile
как барьер памяти для компилятора. Это определенно не стандартно.)
Ответ 2
Самый простой ответ volatile
не требуется для многопоточности.
Длинным ответом является то, что точки последовательности, такие как критические разделы, зависят от платформы, как и любое решение для потоковой обработки, поэтому большая часть вашей безопасности потоков также зависит от платформы.
С++ 0x имеет концепцию потоков и безопасности потоков, но текущий стандарт не делает этого, и поэтому volatile
иногда ошибочно идентифицируется как что-то, что предотвращает переупорядочение операций и доступ к памяти для многопоточного программирования, когда оно никогда не предназначалось и не могут быть надежно использованы таким образом.
Единственное, что нужно использовать volatile
, чтобы в С++ было разрешить доступ к устройствам с отображением памяти, разрешить использование переменных между setjmp
и longjmp
и разрешить использование переменных sig_atomic_t
в обработчиках сигналов. Ключевое слово само по себе не делает переменной атомой.
Хорошие новости в С++ 0x мы будем иметь конструкцию STL std::atomic
, которая может быть использована для обеспечения атомных операций и построений с потоками для переменных. До тех пор, пока ваш компилятор выбора не поддержит его, вам может потребоваться обратиться к библиотеке boost или выкинуть некоторый код сборки, чтобы создать свои собственные объекты для предоставления атомных переменных.
P.S. Большая путаница вызвана тем, что Java и .NET фактически применяют многопоточную семантику с ключевым словом volatile
С++, но следуют примеру C, где это не так.
Ответ 3
Ваша библиотека потоковой передачи должна включать в себя подходящие блоки питания процессора и компилятора при блокировке и разблокировке мьютекса. Для GCC, a memory
clobber в asm-заявлении действует как барьер компилятора.
На самом деле есть две вещи, которые защищают ваш код от (компилятора) кеширования:
- Вы вызываете нечистую внешнюю функцию (
pthread_mutex_*()
), что означает, что компилятор не знает, что эта функция не изменяет ваши глобальные переменные, поэтому она должна перезагрузить их.
- Как я уже сказал,
pthread_mutex_*()
включает в себя барьер компилятора, например: на glibc/x86 pthread_mutex_lock()
заканчивается вызов макроса lll_lock()
, который имеет clobber memory
, заставляя компилятор перезагружать переменные.
Ответ 4
Ключевое слово volatile - это подсказка для компилятора, что переменная может измениться вне логики программы, например, аппаратный регистр с отображением памяти, который может быть изменен как часть процедуры обслуживания прерываний. Это не позволяет компилятору предположить, что кешированное значение всегда корректно и обычно заставляет считывание памяти извлекать значение. Это использование предписывает потоки на пару десятилетий или около того. Я видел, как он использовался с переменными, управляемыми сигналами, но я не уверен, что использование было правильным.
Переменные, защищенные мьютексами, гарантируют правильность при чтении или записи различными потоками. API-интерфейс потоковой передачи необходим для обеспечения согласованности таких представлений переменных. Этот доступ является частью вашей логики программы, а ключевое слово volatile не имеет значения.