Страницы

Поиск по вопросам

Показаны сообщения с ярлыком volatile. Показать все сообщения
Показаны сообщения с ярлыком volatile. Показать все сообщения

воскресенье, 29 марта 2020 г.

Java Где хранится volatile переменная

#java #многопоточность #volatile


Всегда думал что volatile переменные в Java хранятся в MetaSpace, недавно на собеседовании
мне сказали что это неверно. Так вот вопрос: где они хранятся?
    


Ответы

Ответ 1



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

четверг, 27 февраля 2020 г.

Сложение volatile - UB?

#cpp #volatile


Содержит ли следующая программа UB?

#include 

volatile int x;

int main() {
  std::cout << (x + x);
}

    


Ответы

Ответ 1



Да, содержит. Несколько доступов к одному и тому же volatile объекту без упорядочения этих доступов (unsequenced access) - неопределенное поведение. Доступ к volatile объектам испокон веков является частью наблюдаемого поведения (observable behavior) С++ программы. Поэтому доступ к volatile объекту (даже только на чтение) формально считается побочным эффектом (side effect) содержащего этот доступ выражения. А далее уже работает общая схема: наличие в выражении неупорядоченных побочных эффектов, воздействующих на один и тот же объект - это неопределенное поведение. [n4659] 4.6 Program execution [intro.execution] 14 Reading an object designated by a volatile glvalue (6.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.[...] 17 [...]If a side effect on a memory location (4.4) is unsequenced relative to either another side effect on the same memory location or a value computation using the value of any object in the same memory location, and they are not potentially concurrent (4.7), the behavior is undefined.[...] В новой структуре документа: http://eel.is/c++draft/basic.exec#intro.execution-7 http://eel.is/c++draft/basic.exec#intro.execution-10

воскресенье, 23 февраля 2020 г.

Применение и значение ключевого слова volatile

#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, читается/пишется оттуда/туда, где реально находится, без кеширования, например, в регистрах, если это, конечно, возможно.

воскресенье, 16 февраля 2020 г.

Применение и значение ключевого слова volatile

#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, читается/пишется оттуда/туда, где реально находится, без кеширования, например, в регистрах, если это, конечно, возможно.

четверг, 2 января 2020 г.

Можно ли использовать volatile переменную для арифметических операций?

#java #многопоточность #volatile


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

Можно ли так делать? Какие возникнут проблемы?
    


Ответы

Ответ 1



Так делать нельзя. Операции с volatile-переменной не являются атомарными. Ключевое слово volatile лишь сообщает компилятору о том, что переменная может быть изменена либо извне программы, либо другим потоком и нельзя кэшировать её значение, т.е. значение всегда должно считываться/записываться напрямую в ячейку памяти. В качестве примера рассмотрим выполнение кода i = i + 1; Пусть i == 0 и у нас есть два потока, одновременно выполняющих этот код. Тогда возможна следующая ситуация (regX - регистр процессора): Поток 1 Поток 2 Результат mov regA, [i] | | regA == 0 | mov regB, [i] | regB == 0 | add regB, 1 | regB == 1 | mov [i], regB | i == 1 add regA, 1 | | regA == 1 mov [i], regA | | i == 1 То есть, код выполнился два раза, но переменная i тем не менее увеличилась только на единицу. Если Вам нужен потокобезопасный счётчик, используйте AtomicInteger.

Ответ 2



Если переменной только присваивается или читается значение, то никаких проблем не будет. Но если переменная используется как аккумулятор, то возможны сюрпризы. Так код vlt = vlt + 10 преобразовывается в такой int x = vlt; x = x + 10; vlt = x; Соответственно на момент присваивания vlt = x; в поле может лежать любое значение

воскресенье, 22 декабря 2019 г.

volatile register int

#cpp #c #volatile


Имеет ли смысл такая запись?

volatile register int x;


С одной стороны, она компилируется, а с другой - я тут вспоминаю и вроде бы register
не заставляет компилятор размещать переменную в регистре, а лишь даёт рекомендацию,
которую компилятор в праве проигнорировать? И вообще, эти два требования (volatile
и register) независимы или как-то взаимодействуют?

PS: Идея заставить компилятор отключить все оптимизации, связанные с переменной,
но при этом держать её в регистре с целью замера производительности.
    


Ответы

Ответ 1



И вообще, эти два требования (volatile и register) независимы или как-то взаимодействуют? Да, независимы: volatile — это квалификатор типа (type qualifiers), он говорит компилятору, что делать, когда переменной что-то присваивают (когда она является l-value). Другие — это const и restrict (в C99+). register — это спецификатор класса памяти (storage-class specifier). Он говорит компилятору, где разместить память под переменную. Другие — это static, extern, auto (в С), а также де-стандарто typedef (не по смыслу, но так было проще описать грамматику языка). Оных в объявлении переменной может быть не более одного. register не заставляет компилятор размещать переменную в регистре, а лишь даёт рекомендацию, которую компилятор в праве проигнорировать? Да, именно так, разве что, строго говоря, стандарт говорит, даже не «размещать в регистре», а «обеспечить максимально быстрый доступ к ней». Но для большинства компиляторов это очень слабая подсказка, которая принимается оптимизатором, как говорят, чуть чаще, чем никогда. В силу практически полной бесполезности использование этого ключевого слова объявили нежелательным (deprecated) в С++11 и полностью выкинули из последнего стандарта (С++17). На вскидку, на сегодня я бы сказал, что практическая польза от этого слова только одна — низкоуровневый код в gcc с использованием расширенных ассемблерных вставок, чтобы привязать её к конкретному регистру. PS: Идея заставить компилятор отключить все оптимизации, связанные с переменной, но при этом держать её в регистре с целью замера производительности. Почти наверняка не рабочая идея — даже если компилятор действительно разместит переменную в регистре, он не будет уважать все действия с переменной, а только сохранение и загрузку в неё данных, что может смазать результаты замеров. Для таких тонких целей единственный выход — использовать ассемблерные вставки. Во всех противных случаях нельзя контролировать, что именно заменяется.

Ответ 2



Си стандарт 6.7.1 : A declaration of an identifier for an object with storage-class specifier register suggests that access to the object be as fast as possible. The extent to which such suggestions are effective is implementation-defined. The implementation may treat any register declaration simply as an auto declaration. However, whether or not addressable storage is actually used, the address of any part of an object declared with storage-class specifier register cannot be computed, either explicitly (by use of the unary & operator as discussed in 6.5.3.2) or implicitly (by converting an array name to a pointer as discussed in 6.3.2.1). Thus, the only operators that can be applied to an array declared with storage-class specifier register are sizeof and _Alignof. Переменная со знаком register намекает компилятору работать с ней как можно быстрее. Адрес регистровой переменной брать нельзя. Но компиляторы могут реализовать переменную в памяти, а могут в процессорных ячейках. Си стандарт 6.7.3 : An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously. 134) What constitutes an access to an object that has volatile-qualified type is implementation-defined. A volatile declaration may be used to describe an object corresponding to a memory-mapped input/output port or an object accessed by an asynchronously interrupting function. Actions on objects so declared shall not be ‘‘optimized out’’ by an implementation or reordered except as permitted by the rules for evaluating expressions. Переменная со знаком volatile имеет способность менять значение неизвестным компилятору способом. По-этому компилятор постоянно читает и записывает значения в память. Одновременная декларация register volatile указывает обязательно записывать значения. А планы register всего-лишь были увеличить скорость, но не место хранения. Ваши надежды могут быть реализованы наверно только ассемблером.

суббота, 14 декабря 2019 г.

Order of volatile access is undefined in this statement

#c #volatile


Имеются две volatile-переменные:

volatile uint32_t a;
volatile uint32_t b;


Они объявлены как volatile, потому что могут измениться как в основной программе,
так и в обработчике прерывания, так и устройствами на системной шине (такими как DMA).
Если обе переменные участвуют в одном выражении, например:

uint32_t c = a + b;


то компилятор выдаёт предупреждение Order of volatile access is undefined in this
statement. Как я понимаю, это означает, что неизвестно, какая переменная первой загрузится
в регистр общего назначения, и возможна такая ситуация, когда переменная a загрузилась
в регистр, сработало прерывание, изменило это переменную, а основная программа продолжает
работать со старым значением. Некоторые источники рекомендую в таких ситуациях разбивать
выражение на части, например:

uint32_t c = a;
c += b;


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

А теперь собственно вопрос. В программе обработчики прерываний постоянно периодически
изменяют volatile-переменные, а основная программа выполняет математические преобразования.
Как только пришло новое значение переменной, старое уже неинтересно. Можно ли в этом
случае игнорировать предупреждение компилятора? Есть ли у этого предупреждения другие
источники, о которых я не догадываюсь? 
    


Ответы

Ответ 1



В языке С порядок доступа к volatile объектам, наряду с вызовами функций ввода-вывода, является частью наблюдаемого поведения (observable behavior) программы. Поэтому данное предупреждение просто выглядит как предупреждение от педантичного компилятора, который говорит, что наблюдаемое поведение программы в данном случае однозначно не определено. Компилятор прав - оно действительно не определено однозначно. Но компилятор, разумеется, не знает, какие грани наблюдаемого поведения являются действительно важными для специфики вашей программы, а какие нет, и выдает абстрактное педантичное предупреждение. Если в данном случае для вас порядок доступа к этим переменным действительно важен, тогда предупреждение вполне оправданно. Если же вы не видите проблем с любым возможным порядком доступа, то перепишите код чисто ради устранения предупреждения от компилятора. Понятно, что в описанной вами ситуации, при модификации a и b из независимых линий исполнения (потоки, прерывания и т.п.) и при отсутствии какой-либо синхронизации, будет наблюдаться выраженный data race. А уж смертелен ли этот data race для вашего приложения - вам виднее. Если вы захотите избавиться от этого data race, то тут одним подавлением предупреждений не обойдешься - придется организовывать ту или иную форму синхронизации доступа к этой паре.

Ответ 2



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

Ответ 3



Как только пришло новое значение переменной, старое уже неинтересно. У меня была аналогичная проблема. И я решал её приблизительно так: volatile uint32_t a; volatile uint32_t b; uint32_t a1; uint32_t b1; uint32_t x uint8_t trust; // Запоминаем исходные значения a1=a; b1=b; // Выполняем расчёт нужного значения x = a1+b1; // Проверяем, можно ли доверять этому значению if ( (a == a1) && (b == b1) ) trust = 1; else trust = 0; Я понимаю, что с точки зрения теории, это решение - далеко не идеальное. Понимаю, что будут ложные присвоения trust, но если достаточно только уверенности в том. что результат расчёта действительно соответствует ПАРЕ исходных значений, то это почти наверняка. Не 100%, но такая гарантия лучше никакой. А вот если trust==0, то это почти на 100% защитит от ложных результатов.

вторник, 10 декабря 2019 г.

Volatile vs Atomic

#java #concurrency #volatile


В чем разница между модификатором volatile и классами из java.util.concurrent.atomic?
Что такого умеют последние, чего невозможно добиться посредством volatile? Почему?

И приведите, пожалуйста, use case для volatile? В каких ситуациях он (модификатор)
все же востребован и достаточен?
    


Ответы

Ответ 1



volatile обеспечивает только видимость изменений, а классы Atomic* дают еще и атомарность изменений. Простой пример - вам нужно проинкрементить счетчик и вернуть значение. Если поле счетчика будет обычным volatile int - возможна ситуация, когда два разных потока сначала проведут инкремент, а потом оба заберут результат двух инкрементов. Если же взять AtomicInteger, будет гарантирована атомарность, и каждый поток получит правильный результат. Типичное применение volatile: флаги (например, флаг выполнения потока); поля в POJO, которые используются только для хранения данных, когда по какой-то причине нет возможности использовать final-поля.

Ответ 2



volatile не позволяет потокам кэшировать переменную.

воскресенье, 1 декабря 2019 г.

Volatile и кеши процессора

#java #многопоточность #volatile


Здравствуйте, вот возник такой вопрос. Насколько я понял, переменные Volatile обязаны
быть постоянно записаны в память. А если обычная переменная, то она, как правило, хранится
в кеше процессора для более быстрого доступа. Собственно при использовании Volatile
записывается напрямую в оперативку. 
Есть, скажем, какой-то процессор на 4 ядра. Соответственно 4 независимых процессора,
у каждого из которых свои кеши, например, первого и второго уровня, а третий общий.
Ну, собственно, запустили несколько потоков. Допустим, каждый из них работает на отдельном
ядре, у которого свой кеш. Делаем переменную Volatile, и она пишется напрямую в память.
А теперь если запустить программу на одноядерном процессоре, то ОС имитирует параллельность
потоков. Выходит, что кеш у процессора один и все потоки работают на одном процессоре.
Вопрос вот в чем. Переменная обычная кешируется на уровне процессора или на уровне
программном? То есть если даже одно ядро и запущенно несколько потоков, то они делят
кеши процессора на независимые области памяти, имитируя многоядерность. Или же они
используют один и тот же кеш вместе, значит, переменная Volatile не имеет смысла? 
Объясните, пожалуйста, как все происходит. Спасибо. 
EDIT
Я имел ввиду, что переменная для быстрого доступа не пишется сразу в память, например,
x=1  не будет сразу записана в память, так как на это нужно много тактов процессора,
а будет по возможности записана в кеш процессора, а уже потом записана в память. Если
не поставить volatile и если потоки будут одновременно менять ее, то новое значение
будет сохраняться в кеш, а не напрямую в память писаться. Если потоки работают на нескольких
ядрах, то все понятно, у каждого ядра свой кеш, и если попытаться прочитать,  и при
этом последнее значение хранится в каком-то кеше, то получим не то, что надо, если,
конечно, не из того потока читать, который последний записывал. А вот если на одном
ядре, то кеш ведь один и все потоки туда пишут, тогда смысл от volatile? Или я что-то
не так понимаю.    


Ответы

Ответ 1



То есть при попытке чтения если много потоков ее изменили, то у каждого своя кешированная и при попытке прочитать будет не то что надо. При чтении каждый процессор будет видеть локальную копию "переменной в памяти" (из своего кеша), а вот при записи в такую переменную, он сообщит остальным процессорам, что те должны обновить свои кеши в заданной кеш-линии перед последующим чтением. Обычно, такие отношения дают свойство когерентности кэша. Есть процессоры, которые обладают таким свойством, а есть и такие, для которых требуется адаптировать код, чтобы сохранить целостность данных. И одним volatile тут не всегда можно обойтись (на ARM'ах, например). p.s.: только это, опять же, к volatile не имеет отношения. Грубо говоря, это свойство разделяемой памяти SMP-архитектуры. в этой архитектуре, нет места кэшам и проч. занудствам о которых вы пишете с таким завидным упорством @Barmaley, я вот еще немного поразмышлял на тему "нет места кэшам и проч. занудствам" и все таки прихожу к выводу, что без этих знаний Вы врят ли даже на java можете расчитывать на адекватные характеристики производительности Вашего кода для SMP. Вот пример из жизни, с массивами. Но когда я искал этот материал я вообще-то думал даже не о каких-то особенностях строения массивов, а о банальной параллельной обработке классического массива: делим массив на n частей и обрабатываем каждую в своем потоке (с какой-то долей записи в массив, конечно же, т.е., например: "параллельное заполнение данных"). Если Вы не подумаете в рамках этой, простой на первый взгляд, задачи о возможности вытеснения кеш-линии соседним процессором, и порождаемом при этом кеш-промахе, то не получите для нее адекватных результатов (насколько я знаю, java пока не умеет читать мысли программиста и добавлять padding'и под границу кеш-линии для массивов, которые он решит распараллелить таким образом).

Ответ 2



У volatile есть один плюс - он требует, что бы значение переменной всегда читалось с памяти. А в эту память может писать не только процессор... Или второй вариант - есть jni код, который модифицирует переменную, а java код только читает. И чтобы оптимизатор не выкинул чтение, нужно дописать volatile.

Ответ 3



Вам нужно разбить поток мыслей на вопросы. Пока простой ответ который можно дать - volatile не связан с кешем. Ну я имею ввиду обычная переменная ведь в кеше хранится для быстрого доступа? Нет. Правильно думать о кеше и памяти, как о едином месте. Если что-то есть в кеше, оно есть и в памяти (+- пару исключений). В большинстве современных ширпотребовских системах кеш неуправляем, поэтому про него можно просто "забыть". (Если вы программист высоких материй, то можно и не вспоминать). А как на одноядерном происходит если кеш у процессора один по сути все потоки пишут туда, тогда смысл от volatile? Volatile - это высокоуровневый примитив, на котором в java основана MemoryModel, он нужен на любом количестве процессоров, ядер и потоков. (Размер кеша, его количество, иерархичность не влияют.)

Ответ 4



@Alexandr Crospov, для ответа на свои вопросы (они глубже, чем уровень Java) поразбирайтесь с преобразованием виртуальных адресов (именно ими оперирует CPU) в физические (ключевое слово -- MMU), а потом с когерентностью кэшей (начните с ссылки в ответе @mega). -- Кстати, когда говорят о потоках, то обычно подразумевают, что все потоки находятся в одном пространстве виртуальных адресов, а вот у каждого процесса (в нем может быть несколько потоков) свое виртуальное адресное пространство.

Ответ 5



Молодой человек - современное программирование построено на т.н. архитектуре Фон-Неймана - почитайте про это. Там, в этой архитектуре, нет места кэшам и проч. занудствам о которых вы пишете с таким завидным упорством. Место хранения переменной зависит от конкретной реализации конкретного компилятора - так что не забивайте себе голову.

воскресенье, 24 ноября 2019 г.

Ключевое слово volatile в Java


Сегодня встретил такой код
class someClass {

  // ...    
  private volatile int a;
  // ...

}

Вопрос в том, что такое volatile в данном контексте?    


Ответы

Ответ 1



Модификатор volatile накладывает некоторые дополнительные условия на чтение/запись переменной. Важно понять две вещи о volatile переменных: Операции чтения/записи volatile переменной являются атомарными. Результат операции записи значения в volatile переменную одним потоком, становитс виден всем другим потокам, которые используют эту переменную для чтения из нее значения. Кажется, что для человека, задающего вопрос вроде вашего, достаточно знать эти два момента.

Ответ 2



Это означает, что значение переменной будет "всегда читаться". Например, в многопоточны приложениях один поток прочёл значение a=1, передал управление другому потоку, которы изменил значение на a=2, потом управление вернулось. Так вот, без volatile значение a у первого потока будет 1, т.к. первый поток "помнит", что a=1, с volatile - 2, т.к. первый поток снова прочтет значение и получит уже измененное.

Ответ 3



у переменной есть мастер копия плюс по копии на каждую нить, чьл её используют. Мастер копия синкронизируется с локальной копией нити при входе/выходи в/из блока synchronized. Иногда, например, пустой блок synchronized(lock){} имеет смысл. у переменных с модификатором volatile локальных копий нет. Все нити работают с мастер копией.

Ответ 4



Вот какое определение дается в статье «Многопоточность Java» на сайте http://alfalavista.ru. Определение переменной с ключевым словом volatile означает, что значение этой переменной может изменяться другими потоками. Чтобы понять, что делает volatile, полезно разобраться, как потоки обрабатывают обычные переменные. В целях повышения производительности спецификация языка Java допускает сохранение в JRE локальной копии переменной для каждого потока, который на нее ссылается. Такие "локальные" копии переменных напоминают кэш и помогают потоку избежать обращения к главной памяти каждый раз, когда требуется получить значение переменной. При запуске двух потоков один из них считывает переменную A как 5, а второй ― как 10. Если значение переменной А изменилось с 5 на 10, то первый поток не узнае об изменении и будет хранить неправильное значение A. Но если переменная А помечена как volatile, то когда бы поток не считывал значение A, он будет обращаться к главной копии A и считывать ее текущее значение. Локальный кэш потока имеет смысл в том случае, если переменные в ваших приложениях не будут изменяться извне. Если переменная объявлена как volatile, это означает, что она может изменятьс разными потоками. Естественно ожидать, что JRE обеспечит ту или иную форму синхронизаци таких volatile-переменных. JRE действительно неявно обеспечивает синхронизацию при доступе к volatile-переменным, но с одной очень большой оговоркой: чтение volatile-переменной и запись в volatile-переменную синхронизированы, а неатомарные операции ― нет.

Ответ 5



для объектным ссылок volatile можно не писать. я прав? Например, когда мы в многопоточном приложении используем паттерн Синглтон в которо применяем синхронизацию и хотим чтобы синхронизация осуществлялась только один раз при инициализации объекта, а не каждый раз, когда мы вызываем getInstance(), тогда модификатора volatile используем для объектной ссылки: public class Singleton { private static volatile Singleton instance; private Singleton(){ } public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }

Ответ 6



volatile - буквально означает летучий, непостоянный, изменчивый в контексте программирования это означает, что значение переменной может неожиданн изменяться, поэтому не стоит полагаться на значения этой переменной, например, если в коде написано: private volatile int i; // через некоторое время i=0; while(i < 10) { //blah-blah i++; } это не означает, что цикл точно завершится через 10 шагов... вполне может случиться, что в ходе выполнения цикла значение volatile переменно будет неожиданным образом меняться (а может и не будет меняться)...

Ответ 7



volatile - говорит потоку что переменная может меняться, и информирует поток о необходимост обращаться к последней версии, а не к хешированной копии и своевременно распространять изменения. A "volatile" data member informs a thread, both to get the latest value for the variable (instead of using a cached copy) and to write all updates to the variable as they occur.

среда, 9 января 2019 г.

Можно ли использовать volatile переменную для арифметических операций?

Допустим, есть несколько потоков, они прибавляют некоторые значения в volatile-переменную (типа синглтон), и выводят значения этой переменной после суммирования в реальном времени (ну должно быть в реальном времени). Все операции - только с целыми числами.
Можно ли так делать? Какие возникнут проблемы?


Ответ

Так делать нельзя. Операции с volatile-переменной не являются атомарными. Ключевое слово volatile лишь сообщает компилятору о том, что переменная может быть изменена либо извне программы, либо другим потоком и нельзя кэшировать её значение, т.е. значение всегда должно считываться/записываться напрямую в ячейку памяти.
В качестве примера рассмотрим выполнение кода
i = i + 1;
Пусть i == 0 и у нас есть два потока, одновременно выполняющих этот код. Тогда возможна следующая ситуация (regX - регистр процессора):
Поток 1 Поток 2 Результат
mov regA, [i] | | regA == 0 | mov regB, [i] | regB == 0 | add regB, 1 | regB == 1 | mov [i], regB | i == 1 add regA, 1 | | regA == 1 mov [i], regA | | i == 1
То есть, код выполнился два раза, но переменная i тем не менее увеличилась только на единицу.
Если Вам нужен потокобезопасный счётчик, используйте AtomicInteger

понедельник, 12 ноября 2018 г.

Order of volatile access is undefined in this statement

Имеются две volatile-переменные:
volatile uint32_t a; volatile uint32_t b;
Они объявлены как volatile, потому что могут измениться как в основной программе, так и в обработчике прерывания, так и устройствами на системной шине (такими как DMA). Если обе переменные участвуют в одном выражении, например:
uint32_t c = a + b;
то компилятор выдаёт предупреждение Order of volatile access is undefined in this statement. Как я понимаю, это означает, что неизвестно, какая переменная первой загрузится в регистр общего назначения, и возможна такая ситуация, когда переменная a загрузилась в регистр, сработало прерывание, изменило это переменную, а основная программа продолжает работать со старым значением. Некоторые источники рекомендую в таких ситуациях разбивать выражение на части, например:
uint32_t c = a; c += b;
но на мой взгляд, так мы лишь затыкаем компилятор, а не устраняем причину проблемы.
А теперь собственно вопрос. В программе обработчики прерываний постоянно периодически изменяют volatile-переменные, а основная программа выполняет математические преобразования. Как только пришло новое значение переменной, старое уже неинтересно. Можно ли в этом случае игнорировать предупреждение компилятора? Есть ли у этого предупреждения другие источники, о которых я не догадываюсь?


Ответ

В языке С порядок доступа к volatile объектам, наряду с вызовами функций ввода-вывода, является частью наблюдаемого поведения (observable behavior) программы. Поэтому данное предупреждение просто выглядит как предупреждение от педантичного компилятора, который говорит, что наблюдаемое поведение программы в данном случае однозначно не определено.
Компилятор прав - оно действительно не определено однозначно. Но компилятор, разумеется, не знает, какие грани наблюдаемого поведения являются действительно важными для специфики вашей программы, а какие нет, и выдает абстрактное педантичное предупреждение.
Если в данном случае для вас порядок доступа к этим переменным действительно важен, тогда предупреждение вполне оправданно. Если же вы не видите проблем с любым возможным порядком доступа, то перепишите код чисто ради устранения предупреждения от компилятора.
Понятно, что в описанной вами ситуации, при модификации a и b из независимых линий исполнения (потоки, прерывания и т.п.) и при отсутствии какой-либо синхронизации, будет наблюдаться выраженный data race. А уж смертелен ли этот data race для вашего приложения - вам виднее. Если вы захотите избавиться от этого data race, то тут одним подавлением предупреждений не обойдешься - придется организовывать ту или иную форму синхронизации доступа к этой паре.