Для чего используется ключевое слово "volatile"?

Я прочитал несколько статей о ключе volatile, но я не смог понять его правильное использование. Не могли бы вы рассказать мне, для чего он должен использоваться в С# и Java?

Ответ 1

Как для С#, так и для Java, "volatile" сообщает компилятору, что значение переменной никогда не должно быть кэшировано, поскольку его значение может измениться вне области самой программы. Затем компилятор избегает любых оптимизаций, которые могут привести к проблемам, если переменная изменяется "вне ее контроля".

Ответ 2

Рассмотрим следующий пример:

int i = 5;
System.out.println(i);

Компилятор может оптимизировать это, чтобы просто напечатать 5, например:

System.out.println(5);

Однако, если есть другой поток, который может изменить i, это неправильное поведение. Если другой поток изменяет i на 6, оптимизированная версия все равно будет печатать 5.

Ключевое слово volatile предотвращает такую ​​оптимизацию и кеширование и, следовательно, полезно, когда переменную можно изменить другим потоком.

Ответ 3

volatile ключевое слово имеет разные значения как в Java, так и в С#.

Java

Из Java Language Spec:

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

С#

Из справочника С# в ключе volatile:

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

Ответ 4

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

  • Переменная является энергонезависимой.

Когда два потока A и B обращаются к энергонезависимой переменной, каждый поток будет поддерживать локальную копию переменной в локальном кеше. Любые изменения, сделанные потоком A в нем локальным кешем, не будут видны потоку B.

  • Переменная нестабильна

Когда переменные объявляются volatile, это по существу означает, что потоки не должны кэшировать такую ​​переменную, или, другими словами, потоки не должны доверять значениям этих переменных, если они не будут непосредственно прочитаны из основной памяти.

Итак, когда нужно сделать переменную volatile?

Когда у вас есть переменная, к которой можно получить доступ по многим темам, и вы хотите, чтобы каждый поток получал последнее обновленное значение этой переменной, даже если значение обновляется любым другим потоком/процессом/вне программы.

Ответ 5

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

В записях летучих полей есть семантика выпуска. Это означает, что гарантируется, что любая память, записываемая в изменчивую переменную, будет задерживаться до тех пор, пока все предыдущие записи в памяти не будут видны другим процессорам.

Рассмотрим следующий пример:

something.foo = new Thing();

Если foo является переменной-членом в классе, а другие ЦП имеют доступ к экземпляру объекта, на который ссылается something, они могут видеть значение foo изменить до в памяти записи в конструкторе Thing отображаются во всем мире! Это то, что означает "слабо упорядоченная память". Это может произойти, даже если компилятор имеет все хранилища в конструкторе перед хранилищем до foo. Если foo - volatile, то в хранилище до foo будет семантика выпуска, а аппаратное обеспечение гарантирует, что все записи до записи в foo будут видны другим процессорам, прежде чем разрешить запись в foo происходят.

Как можно так сильно переупорядочить записи в foo? Если строка кэша, содержащая foo, находится в кеше, а магазины в конструкторе пропустили кеш, тогда хранилище может завершиться намного раньше, чем записи в кэш не пройдут.

(ужасная) архитектура Itanium от Intel имела слабо упорядоченную память. Процессор, используемый в оригинальном XBox 360, имел слабо упорядоченную память. Многие процессоры ARM, включая очень популярный ARMv7-A, имеют слабо упорядоченную память.

Разработчики часто не видят эти данные, потому что такие вещи, как блокировки, будут делать полный барьер памяти, по сути, то же самое, что и семантика получения и выпуска. Никакие нагрузки внутри замка не могут быть спекулятивно выполнены до того, как блокировка будет получена, они будут отложены до фиксации замка. Никакие магазины не могут быть задержаны через блокировку, инструкция, которая освобождает блокировку, задерживается до тех пор, пока все записи, сделанные внутри блокировки, не будут видны на глобальном уровне.

Более полным примером является шаблон с двойной проверкой блокировки. Цель этого шаблона - избежать необходимости всегда приобретать блокировку, чтобы ленить инициализировать объект.

Захват из Википедии:

public class MySingleton {
    private static object myLock = new object();
    private static volatile MySingleton mySingleton = null;

    private MySingleton() {
    }

    public static MySingleton GetInstance() {
        if (mySingleton == null) { // 1st check
            lock (myLock) {
                if (mySingleton == null) { // 2nd (double) check
                    mySingleton = new MySingleton();
                    // Write-release semantics are implicitly handled by marking mySingleton with
                    // 'volatile', which inserts the necessary memory barriers between the constructor call
                    // and the write to mySingleton. The barriers created by the lock are not sufficient
                    // because the object is made visible before the lock is released.
                }
            }
        }
        // The barriers created by the lock are not sufficient because not all threads will
        // acquire the lock. A fence for read-acquire semantics is needed between the test of mySingleton
        // (above) and the use of its contents.This fence is automatically inserted because mySingleton is
        // marked as 'volatile'.
        return mySingleton;
    }
}

В этом примере хранилища в конструкторе MySingleton могут быть недоступны для других процессоров, прежде чем хранилище будет MySingleton. Если это произойдет, другие потоки, которые заглядывают в mySingleton, не получат блокировку, и они не обязательно будут записывать записи в конструктор.

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

Ответ 6

В Java "volatile" используется, чтобы сообщить JVM, что переменная может использоваться несколькими потоками одновременно, поэтому некоторые общие оптимизации не могут быть применены.

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