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

130

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

Мирча
источник
1
Одна из проблем с volatile в том, что он означает больше чем одно. Информация для компилятора о том, что не следует выполнять фанковые оптимизации, - это наследие C. Это также означает, что при доступе следует использовать барьеры памяти. Но в большинстве случаев это просто снижает производительность и / или сбивает людей с толку. : P
AnorZaken

Ответы:

93

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

Будет А
источник
@Tom - должным образом отмечено, сэр - и исправлено.
Will A,
11
Это все еще намного более тонкое, чем это.
Том Хотин - tackline
1
Неправильно. Не препятствует кешированию. Смотрите мой ответ.
doug65536 07
168

Рассмотрим этот пример:

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

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

System.out.println(5);

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

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

Сьерд
источник
3
Я считаю , что оптимизация будет по- прежнему действует с iпометкой volatile. В Java все сводится к отношениям « произошло до» .
Том Хотин - tackline
Спасибо за публикацию, так почему-то volatile имеет соединения с переменной блокировкой?
Мирча,
@Mircea: Это то, о чем мне сказали, что пометка чего-то как изменчивого заключалась в том, что для маркировки поля как изменчивого будет использоваться некоторый внутренний механизм, позволяющий потокам видеть согласованное значение для данной переменной, но это не упоминается в ответе выше ... может кто подтвердит это или нет? Спасибо
npinti 07
5
@Sjoerd: Я не уверен, что понимаю этот пример. Если iэто локальная переменная, никакой другой поток не может ее изменить. Если это поле, компилятор не сможет оптимизировать вызов, если это не так final. Я не думаю, что компилятор может производить оптимизацию, исходя из предположения, что поле «выглядит», finalкогда оно явно не объявлено как таковое.
polygenelubricants
1
C # и java - это не C ++. Это не так. Это не предотвращает кеширование и не препятствует оптимизации. Речь идет о семантике чтения-получения и сохранения-выпуска, которые требуются в слабо упорядоченных архитектурах памяти. Речь идет о спекулятивной казни.
doug65536 07
40

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

  • Переменная энергонезависимая

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

  • Переменная непостоянна

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

Итак, когда сделать переменную изменчивой?

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

Саураб Патил
источник
2
Неправильно. Это не имеет ничего общего с «предотвращением кеширования». Речь идет о переупорядочении компилятором ИЛИ аппаратного обеспечения ЦП посредством спекулятивного выполнения.
doug65536 07
37

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

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

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

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никогда не мешает кешированию. Что он делает, так это гарантирует порядок, в котором другие процессоры «видят» записи. Освобождение хранилища будет задерживать сохранение до тех пор, пока все ожидающие записи не будут завершены и не будет запущен цикл шины, сообщающий другим процессорам, что они должны сбросить / записать свою строку кэша, если у них есть кэшированные соответствующие строки. Приобретение нагрузки сбрасывает все предполагаемые чтения, гарантируя, что они не будут устаревшими значениями из прошлого.

doug65536
источник
Хорошее объяснение. Также хороший пример блокировки с двойной проверкой. Однако я все еще не уверен, когда использовать, поскольку меня беспокоят аспекты кеширования. Если я напишу реализацию очереди, в которой только 1 поток будет писать и только 1 поток будет читать, могу ли я обойтись без блокировок и просто пометить свои «указатели» головы и хвоста как изменчивые? Я хочу убедиться, что и читатель, и писатель увидят самые свежие значения.
nickdu
И то, headи другое tailдолжно быть изменчивым, чтобы производитель не предполагал, tailчто не изменится, и чтобы потребитель не предполагал, headчто не изменится. Кроме того, он headдолжен быть энергозависимым, чтобы гарантировать, что записи данных очереди будут глобально видимы до того, как сохранение headбудет глобально видимым.
doug65536
+1, такие термины, как последний / "самый последний", к сожалению, подразумевают концепцию единственного правильного значения. В действительности два конкурента могут пересечь финишную черту в одно и то же время - на центральном процессоре два ядра могут запрашивать запись в одно и то же время . В конце концов, ядра не выполняют работу по очереди - это сделало бы многоядерность бессмысленной. Хорошее многопоточное мышление / дизайн не должны сосредотачиваться на попытках заставить низкоуровневую "новизну" - по сути своей фальшивкой, поскольку блокировка просто заставляет ядра произвольно выбирать одного динамика за раз без справедливости - а скорее попытаться спроектировать потребность в таком неестественном понятии.
AnorZaken
34

Летучее ключевое слово имеет различные значения в обоих Java и C #.

Ява

Из спецификации языка Java :

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

C #

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

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

Крок
источник
Большое спасибо за публикацию, как я понял в Java, это действует как блокировка этой переменной в контексте потока, а в C #, если используется, значение переменной может быть изменено не только из программы, внешние факторы, такие как ОС, могут изменять ее значение ( без блокировки) ... Пожалуйста, дайте мне знать, правильно ли я понял эти различия ...
Мирча,
@Mircea в Java не задействует блокировку, она просто гарантирует, что будет использоваться самое последнее значение изменчивой переменной.
krock 07
Обещает ли Java какой-то барьер для памяти, или это похоже на C ++ и C #, только обещая не оптимизировать ссылку?
Стивен Судит,
Барьер памяти - это деталь реализации. На самом деле Java обещает, что все операции чтения будут видеть значение, записанное последней записью.
Stephen C
1
@StevenSudit Да, если аппаратному обеспечению требуется барьер или загрузка / получение или сохранение / выпуск, тогда оно будет использовать эти инструкции. Смотрите мой ответ.
doug65536 07
9

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

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

Торбьёрн Равн Андерсен
источник
1

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

Субхаш Шайни
источник
1
Вы можете перефразировать свой ответ?
Anirudha Gupta
Ключевое слово volatile предоставит вам наиболее актуальное значение, а не кешированное значение.
Субхаш Шайни
0

Volatile решает проблему параллелизма. Чтобы это значение синхронизировалось. Это ключевое слово чаще всего используется в потоках. Когда несколько потоков обновляют одну и ту же переменную.

Шив Ратан Кумар
источник
1
Не думаю, что это «решает» проблему. Это инструмент, который помогает в некоторых обстоятельствах. Не полагайтесь на volatile в ситуациях, когда требуется блокировка, например, в состоянии гонки.
Scratte