Для чого використовується "нестабільне" ключове слово?


130

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


1
Одна з проблем з летючою є те, що це означає більше, ніж одне. Це відомості компілятору про те, щоб не робити прикольних оптимізацій - це спадщина C. Це також означає, що для доступу слід використовувати бар'єри пам'яті. Але в більшості випадків це просто коштує продуктивності та / або заплутує людей. : P
AnorZaken

Відповіді:


93

Як для C #, так і для Java, "непостійний" повідомляє компілятору, що значення змінної ніколи не слід кешувати, оскільки її значення може змінюватися поза межами сфери самої програми. Потім компілятор уникне будь-яких оптимізацій, які можуть спричинити проблеми, якщо змінна зміниться "поза її контролем".


@ Том - належним чином зауважив, сер - і доповнено.
Чи буде

11
Це все ще набагато тонкіше за це.
Том Хотін - тайклін

1
Неправильно. Не запобігає кешування. Дивіться мою відповідь.
doug65536

168

Розглянемо цей приклад:

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

Компілятор може оптимізувати це для простого друку 5, як це:

System.out.println(5);

Однак якщо є інший потік, який може змінитися i, це неправильна поведінка. Якщо інший потік зміниться iна 6, оптимізована версія все ще надрукує 5.

volatileКлючове слово запобігає таку оптимізацію і кешування, і , таким чином , є корисним , коли змінна може бути змінена іншим потоком.


3
Я вважаю, що оптимізація все ще буде дійсною з iпозначкою як volatile. На Яві все йде про стосунки, що трапляються раніше .
Том Хотін - тайклін

Дякуємо за публікацію, тож якось непостійний зв'язок із блокуванням змінної?
Мірча

@Mircea: Це те, що мені сказали, що для позначення чогось як мінливого було все: маркування поля як летючого використовує якийсь внутрішній механізм, щоб потоки могли бачити послідовне значення для даної змінної, але це не згадується у відповіді вище ... можливо, хтось може це підтвердити чи ні? Спасибі
npinti

5
@Sjoerd: Я не впевнений, що розумію цей приклад. Якщо iце локальна змінна, жоден інший потік не може її змінити. Якщо це поле, компілятор не може оптимізувати виклик, якщо це не так final. Я не думаю, що компілятор не може робити оптимізацію, грунтуючись на припущенні, що поле "виглядає", finalколи воно прямо не оголошено як таке.
полігенмастильні матеріали

1
C # і java не є C ++. Це неправильно. Це не перешкоджає кешування і не заважає оптимізації. Йдеться про читання-придбання та зберігання-випуск семантики, які потрібні для слабо упорядкованих архітектур пам'яті. Йдеться про спекулятивне страта.
doug65536

40

Щоб зрозуміти, що мінливий робить змінною, важливо зрозуміти, що відбувається, коли змінна не є мінливою.

  • Змінна - енергонезалежна

Коли два потоки A & B звертаються до енергонезамінної змінної, кожен потік буде підтримувати локальну копію змінної у своєму локальному кеші. Будь-які зміни, внесені потоком A в її локальний кеш, не будуть видимі для потоку B.

  • Змінна є мінливою

Коли змінні оголошуються мінливими, це по суті означає, що потоки не повинні кешувати таку змінну, або іншими словами, потоки не повинні довіряти значенням цих змінних, якщо вони не зчитуються безпосередньо з основної пам'яті.

Отже, коли зробити змінну мінливою?

Коли у вас є змінна, до якої можна отримати доступ до багатьох потоків, і ви хочете, щоб кожен потік отримував останнє оновлене значення цієї змінної, навіть якщо значення оновлюється будь-яким іншим потоком / процесом / поза програмою.


2
Неправильно. Це не має нічого спільного з "запобіганням кешування". Йдеться про переупорядкування компілятором АБО процесорного обладнання шляхом спекулятивного виконання.
doug65536

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ніколи не запобігає кешування. Те, що він робить, - це гарантувати порядок, в який "бачать" інші процесори. Випуск магазину затримує сховище до тих пір, поки всі очікувані записи не будуть завершені і не буде видано цикл шини, який повідомляє іншим процесорам скасувати / записувати їх кеш-рядок, якщо у них трапляється кешування відповідних рядків. Придбання вантажу вимиє будь-які роздуми, що читаються, гарантуючи, що вони не будуть усталеними значеннями минулого.


Гарне пояснення. Також хороший приклад блокування подвійної перевірки. Однак я все ще не впевнений, коли використовувати, оскільки я переживаю за аспекти кешування. Якщо я напишу реалізацію черги, де буде писатись лише 1 нитка, і лише 1 нитка буде читати, чи можу я пройти без замків і просто позначити голову та хвіст «покажчиками» як мінливими? Я хочу переконатися, що і читач, і письменник бачать найбільш сучасні цінності.
нікду

І те, headі виробництво tailповинно бути мінливим, щоб не допустити, що виробник припуститься tail, що не зміниться, і щоб споживач не припускав head, що не зміниться. Крім того, вони headповинні бути мінливими, щоб гарантувати, що записи даних черги будуть видимими у всьому світі, перш ніж магазин headбуде глобально видимим.
doug65536

+1, терміни, як останній / "найновіший", на жаль, передбачають поняття єдиного правильного значення. Насправді два конкуренти можуть перетнути фінішну лінію в той самий час - на процесорі два ядра можуть запитати запитання в той самий час . Зрештою, сердечники не по черзі роблять роботу - це зробить багатоядерними безглузді. Гарне багатопотокове мислення / дизайн не повинно зосереджуватися на намаганні змусити "найсвіжіші" на низькому рівні - це властиво підробкою, оскільки замок просто змушує ядра довільно вибирати один динамік одночасно без справедливості, - а намагатися спроектувати необхідність такої неприродної концепції.
AnorZaken

34

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

Java

З специфікації мови Java :

Поле може бути оголошено мінливим, і в цьому випадку модель пам'яті Java гарантує, що всі потоки бачать послідовне значення для змінної.

C #

З посилання на C # на летюче ключове слово :

Ефірне ключове слово вказує на те, що поле може бути змінено в програмі чимось таким, як операційна система, апаратне забезпечення або одночасно виконуваний потік.


Дуже дякую за публікацію, як я зрозумів в Java, вона діє як блокування цієї змінної в контексті потоку, а в C #, якщо використовується значення, змінна може бути змінена не тільки з програми, зовнішні фактори, такі як ОС, можуть змінити її значення ( ніякого блокування не мається на увазі) ... Будь ласка, дайте мені знати, чи я правильно зрозумів ці відмінності ...
Mircea

@Mircea в Java немає блокування, воно просто гарантує, що буде використано найбільш актуальне значення мінливої ​​змінної.
крок

Чи обіцяє Java якийсь бар'єр пам’яті чи це як C ++ та C # лише обіцяє не оптимізувати посилання?
Стівен Судіт

Бар'єр пам'яті - це деталь реалізації. Що насправді обіцяє Java, це те, що всі прочитані побачать значення, написані самим останнім записом.
Стівен С

1
@StevenSudit Так, якщо обладнання потребує бар'єру або завантаження / придбання або зберігання / випуску, воно використовуватиме ці інструкції. Дивіться мою відповідь.
doug65536

9

У Java "летюча" використовується для того, щоб сказати JVM, що змінна може використовуватися одночасно декількома потоками, тому певні загальні оптимізації неможливо застосувати.

Зокрема, ситуація, коли два потоки, що мають доступ до однієї змінної, працюють на окремих процесорах в одній машині. Це дуже часто для процесора, щоб він кешував агресивно дані, які він зберігає, оскільки доступ до пам'яті набагато повільніше, ніж доступ до кешу. Це означає, що якщо дані оновлюються в CPU1, вони повинні негайно пройти через усі кеші та в основну пам'ять, а не тоді, коли кеш вирішить очистити себе, щоб CPU2 побачив оновлене значення (знову ж таки, нехтуючи всіма кешами на шляху).


1

Коли ви читаєте нелетучі дані, виконуючий потік може або не завжди отримує оновлене значення. Але якщо об’єкт є летючим, нитка завжди отримує найсучасніше значення.


1
Чи можете ви перефразовувати свою відповідь?
Анірудха Гупта

нестабільне ключове слово дасть вам найбільш оновлене значення, а не кешоване значення.
Subhash Saini

0

Нестабільна - це вирішення задачі про одночасність. Щоб це значення було синхронізовано. Це ключове слово в основному використовується для нарізування різьби. При декількох потоках оновлення однієї змінної.


1
Я не думаю, що це "вирішує" проблему. Це інструмент, який допомагає за певних обставин. Не покладайтеся на непостійні у ситуаціях, коли потрібен замок, як у перегонах.
Скретт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.