#c_sharp #многопоточность #volatile
Если читать горячо любимый msdn можно найти следующую формулировку:
Ключевое слово volatile указывает, что поле может быть изменено
несколькими потоками, выполняющимися одновременно. Поля, объявленные
как volatile, не проходят оптимизацию компилятором, которая
предусматривает доступ посредством отдельного потока. Это гарантирует
наличие наиболее актуального значения в поле в любое время.
А также на стороннем ресурсе есть такая :
Согласно MSDN ключевое слово volatile указывает, что поле может быть
изменено несколькими потоками, выполняющимися одновременно и поэтому
JIT компилятор не будет производить оптимизации с полем
Поправьте, если что пожалуйста, ибо я немного запутался, моя логика следующая : я
могу прямо не знать что значение поля модифицируется, когда значение реально модифицируется
и из разных потоков. Я говорю компилятору что мне необходимо читать поле не из кеша,
ибо из кеша может попасть не валидное значение, а читать прямо из области где она лежит,
что дает мне право в любой момент времени, независимо от того менялось оно или не менялось,
получать действительно верное значение поля.
Ответы
Ответ 1
C volatile все не так просто, как кажется, т.к. делает он совсем не то, что volatile
в C++. Пробивание кэшей является только сайдэффектом, и срабатывает не совсем так,
вы этого ожидаете.
Тем не менее, он используется чаще всего ради пробивания кэшей, и даже в MSDN по
volatile он приведен именно на примере "пробивания кэша" при чтении свойства.
private volatile bool _shouldStop;
// в одном потоке
while (!_shouldStop)
{
Console.WriteLine("Worker thread: working...");
}
// в другом потоке
_shouldStop = true;
Но при этом тот же пример из MSDN отлично работает, если слово volatile убрать. В
чем подвох?
Что на самом деле делает volatile, и почему он "пробивает кэш"? И пробивает ли он
его вообще, и существуют ли способы "пробить кэш"?
В спецификации C# volatile упоминается в паре мест - §3.10 Execution order и §10.5.3
Volatile fields.
3.10 Execution order
Execution of a C# program proceeds such that the side effects of each executing
thread are preserved at critical execution points. A side effect is defined as a read
or write of a volatile field, a write to a non-volatile variable, a write to an external
resource, and the throwing of an exception. The critical execution points at which
the order of these side effects must be preserved are references to volatile fields
(§10.5.3), lock statements (§8.12), and thread creation and termination.
По сути, оптимизатор не может переносить запись или чтение volatile поля за lock,
переносить его через throw и еще через пару определенных конструкций. Ни слова о кешировании.
т.е. в ситуации
... много кода без critical execution points (локов, работы с volatile и прочим)
volatile read
оптимизатор волен сделать
volatile read
... много кода без critical execution points (локов, работы с volatile и прочим)
и даже lock в этом случае не поможет:
... много кода без critical execution points, сайдэффектов и чтения памяти
lock
{
volatile read
}
законно превращается в
lock
{
volatile read
... много кода без critical execution points, сайдэффектов и чтения памяти
}
ок, вторая часть спеки
10.5.3 Volatile fields
A read of a volatile field is called a volatile read. A volatile read has “acquire
semantics”; that is, it is guaranteed to occur prior to any references to memory that
occur after it in the instruction sequence.
Опять не слова про кэширование. Утверждается что volatile read произойдет не позже,
чем он написан в коде (относительно другого доступа к памяти). Гораздо раньше - без
проблем!
По сути, volatile запрещает оптимизатору переставлять все обращения к volatile-переменным
местами (с друг другом, и с другими обращениями к памяти). Для не-volatile переменных
подобные перестановки разрешены.
т.е. при выполнении кода
// в одном потоке
a = 1;
b = 1
a = 2;
b = 2;
и
// в другом потоке
Console.WriteLine($"{a} {b}");
для не-volatile переменных вы можете получить ... "2 1"
Запрет такой перестановки - это основное предназначение volatile. Именно так он задуман,
и именно в таком виде вписан в спецификацию.
Ок, но он ведь запрещает кэширование? Как он это делает? Что же запрещает рантайму
превратить
while (!_shouldStop)
{
Console.WriteLine("Worker thread: working...");
}
в
регистр = _shouldStop;
while (регистр)
{
Console.WriteLine("Worker thread: working...");
}
Мешает ему раздел стандарт ECMA-335, раздел I.12.6.7 Volatile reads and writes
An optimizing compiler that converts CIL to native code shall not remove any volatile
operation,
nor shall it coalesce multiple volatile operations into a single operation.
Оптимизатору JIT просто запрещено заменять несколько чтений (в цикле) одним. Что,
на практике, заставляет его вычитывать значение из памяти при каждом упоминании этого
значения в коде. Что приводит к "пробиванию кэша" - запрету на использования значения
из регистра, вычитанного при прошлом обращении к полю.
Точно так же, сайдэффектом, "кэш" пробивается lock-ом:
lock
{
не-volatile read
}
Acquiring a lock shall implicitly perform a volatile read operation
что запрещает оптимизатору переставить обращение к памяти чуть повыше.
Ответ 2
Ну, в общем, правильно.
Всё, что помечено как volatile, читается/пишется оттуда/туда, где реально находится,
без кеширования, например, в регистрах, если это, конечно, возможно.