Есть True и False sharing которые позволяет процессорам обмениваться кэш-линиями. Как при этом может существовать проблема visibility?
Если sharing позволяет ядрам видеть кэши друг друга, то в чем тогда проблема которую решает volatile?
Или можно еще перефразировать так: чего не хватает sharing механизмам, чтобы в состоянии контролируемой гонки предотвратить утечку данных?
Возможно, что sharing-то работает, но он работает только для тех процессоров, которы
уже имеют данную переменную в кэше, а потоки-новобранцы могут прочитать данные из памяти, которые уже не актуальны, так как те потоки, что уже давно работают с этой переменной — успели изменить ее значение после последней выгрузки в память?
То есть в промежутке между первым считыванием из памяти переменной и её первого
изменения шаринг не срабатывает? (как гипотеза).
Update:
"если протокол согласованности кэшей (cache coherency) обязывает кэши процессор
хранить ячейку памяти в согласованном состоянии, то зачем нужен volatile, который делает то же самое?"
Ответы
Ответ 1
False Sharing
False Sharing это термин, описывающий механизм нежелательного снижения производительности, когда разные потоки модифицируют независимые переменные, которые оказались на одной кеш линии.
Прочитайте замечательную стататью с Хабра, где описаны эти механизмы. Если кратко, то вот цитата:
При этом, если один из потоков модифицирует поле своей структуры, то
вся кэш-линия в соответствии с cache coherency протоколом объявляется
невалидной для остальных ядер процессора. Другой поток уже не сможет
пользоваться своей структурой, несмотря на то, что она уже лежит в L1
кэше его ядра. В старых процессорах типа P4 в такой ситуации
потребовалась бы долгая синхронизация с основной памятью, то есть
модифицированные данные были бы отправлены в основную память и потом
считаны в L1 кэш другого ядра.
Volatile
Модификатор volatile это ключево слово в языке java, введенное в язык для поддержк
безопасного многопоточного программирования. Оно накладывает некоторые дополнительные условия на чтение/запись переменной. Важно понять три вещи о volatile переменных:
Операции чтения/записи volatile переменной являются атомарными.
Результат операции записи значения в volatile переменную одним потоком, становитс
виден всем другим потокам, которые используют эту переменную для чтения из нее значения.
Ключевое слово volatile запрещает некоторые оптимизации/перестановки в процессоре и/или компиляторе.
Т.е. сравнивать эти понятия не совсем корректно ибо volatile это слово для реализаци
безопасных многопоточных пограмм, а false sharing это термин описывающий деградацию производительности.
Посмотрите замечательные лекции от Алексея Шипилева по java memory model (и не только), где он все раскладывает по полочкам.
Если у Вас будут вопросы, то я могу попробовать раскрыть обновляя свой ответ.
UPD: Ответы на вопросы внизу.
а где в спеке написано что volatile гарантирует нам атомарность
операций?
Ссылки: Essentials от Oracle, спецификация
Разве не для того мы ставим synchronize чтобы предотвратить
последствия не атомарности? Если бы volatile гарантировал атомарность
то он бы один решал все проблемы, так получается или нет?
Важно понять, что существуют два аспекта потокобезопасности: (1) контроль выполнени
и (2) видимость памяти. Первый отвечает за контроль выполнения кода (включая порядо
инструкций) и разрешая/запрещая некоторым блокам программы возможность выполняться конкурентн
(concurrently /одновременно). Второе какие действия с памятью видны или не видны для других потоков. Это вызвано тем, что каждый процессор имеет несколько уровней кеша между самим процессором и общей памятью, поэтому потоки запущенные на разных ядрах процессора могут видеть "разную память" в один и тот же момент из-за локального кеша процессора.
Synchronized
Использование synchronized не позволяет другому процессу захватить монитор (или lock
на том же самом обьекте, таким образом препятствуя конкуррентному (одновременному) выполнени
кода, заключенного в synchronized блок. Важно учесть, что синхронизация создает так называемое отношение happens-before. Это отношение позволяет потоку захватившему монитор "увидеть" все изменения, сделанные другим потоком до захвата и отпускания (release) монитора.
На практике же это будет соответствовать (грубое приближение) тому, что процессо
будет обновлять кеши в момент захвата монитора и записывать в память после его освобождения. Эти оперции довольно долгие (относительно).
Volatile
Ипользование volatile заставляет делать операции с переменной используя память програм
"минуя" кеш процессора. Это может быть полезно, когда нам нужна видимость этой переменно
в разных потоках, но при этом нам не важен парядок доступа к данной переменной. Также на 32bit java запись long & double становится атомарной при обьявлении переменной как volatile. В новой спецификации JSR-133 (в Java5) семантику volatile усилили. На нее наложили правила видимости и правила запрещающие некоторые опитимизации компилятора/jvm.
Примеры
Volatile - поможет
Предположим у нас есть какой-то неизменяемый обьект, ссылка на который доступн
для множества потоков, и они постоянно используют его в своих вычилениях. Volatile отличн
подходит для данной ситуации. Необходимо, чтобы другие потоки стали использовать новый обьект как только он будет обьявлен (в данном месте имею ввиду, что мы поменяем ссылку с существующего обьекта на сконтруированный новый). При этом нам не требуется специльно синхронизировать это обновление, сбрасывать кеши.
Volatile - не поможет
Возьмем обычный счетчик:
volatile int counter = 0;
public void update() {
counter++; //или counter = counter + 1;
}
Операция инкремента неатомарна и состоит из трех операций: чтение, инкремент, запись. В данном примере может случиться ситуация, когда:
Поток1: заходит в метод читает значение "0";
Поток1: увеличивает значение на единицу "1";
Выполнение переходит к второму потоку;
Поток2: чтение значения "0";
Поток2: увеличение значения на единицу "1";
Поток2: запись "1" в counter;
Выполнение переходит к первому потоку;
Поток1: запись "1" в counter;
В результате вместо значения "2" в счетчике хранится значение "1".
В данном случаче поможет синхронизация метода update() или использование AtomicInteger и т.д. это уже за пределами данного вопроса.
Подытоживая все вышескзанное - volatile переменные используются, когда все операци
происходящие с обьектом "атомарны" как в первом примере (меняется ссылка на полностью сформированный обьект, идет запись из одного одного потока) и нет конкуренции за состояние обьекта.
Ответ 2
Напишу пару уточнений.
Насколько понимаю, сам вопрос звучит немного по-другому: "если протокол согласованност
кэшей (cache coherency) обязывает кэши процессора хранить ячейку памяти в согласованном состоянии, то зачем нужен volatile, который делает то же самое?"
Во-первых, тут идет обсуждение двух разных уровней. JLS действует внутри JVM, протоко
cache coherence присутствует только в специфичной процессорной архитектуре. Cache coherenc
не обязан существовать на той архитектуре, для которой скомпилирована и на которой запускаетс
JVM, таким образом JLS делает опциональную фичу обязательной (на самом деле, фича чут
больше чем просто согласованность, про это ниже). Я практически уверен, что 99%+ многоядерны
процессоров сейчас имеют этот протокол, однако Java не может полагаться на что-то, чему нет гарантии - предполагается, что все приложения на Java должны исполняться одинаково на всех архитектурах (кроме случаев взаимодействия с ОС, где могут быть, например, разные пути). Поэтому JLS был практически обязан ввести такое понятие, даже если оно существует на большинстве систем из коробки, потому что даже если JVM реализована на каком-нибудь питоне, она все равно должна исполнять код так же, как и на любой другой системе.
Во-вторых, если взять определение из википедии:
a multiprocessor is cache consistent if all writes to the same memory locatio
are performed in some sequential order
(вольный перевод) многопроцессорный кэш является консистентным, если все операции записи по одному адресу выполняется в каком-либо последовательном порядке
то здесь стоит обратить внимание на "memory location". В Java присутствуют типы данных
которые могут занимать больше одного слова, которым оперирует процессор - как минимум, при запуске на 32битной операционной системе double и long будут занимать по два слова. Если я все правильно понимаю, то на такой системе может возникнуть следующая ситуация:
линия кэша 1: <другие данные><старшие или младшие 32 бита double>
линия кэша 2: <остаток double><другие данные>
В этом случае процессор даже в условиях строгого cache coherence имеет право обновит
ровно половину double, в результате чего потоки имеют право увидеть мусор вместо реального значения. Volatile запрещает такую ситуацию, гарантируя атомарность записи любой переменной.
В-третьих, кроме непосредственно "железных" проблем, в выполнении кода (косвенно
участвует компилятор. Я не знаю, насколько это применимо к современной Java, но агрессивный компилятор имеет право применить следующие оптимизации:
boolean flag = true;
while (flag) {
doProcessing();
}
// хм, flag не отмечен volatile, значит, программист считает, что он может обновляться только локально
// закэширую-ка я его в регистре процессора, так будет быстрее
eax = load(flag);
while (eax) {
doProcessing();
}
регистр при этом никогда не обновится - он никак не связан с протоколом целостност
кэша. Повторюсь, что я не знаю, как реально ведут себя существующие компиляторы Java, но именно этот пример приведен в JLS как небезопасный.
Ну и, наконец, семантика volatile вмешивается в порядок выполнения программы. JLS требует выполнения следующих условий:
Все действия внутри одного треда имеют зависимость happens-before друг с другом
т.е. результат вышестоящего по коду действия всегда будет виден нижестоящему по коду действию.
Все действия с volatile имеют зависимость happens-before друг к другу - если кто-т
записал в volatile-поле некоторое значение, все последующие чтения уже не имеют право увидеть устаревшее значение
Отношение happens-before транзитивно, т.е. если операция A happens-before B, а
happens-before C, то справедливо A happens-before C - значит, C увидит все изменения, сделанные A.
Компилятор, JVM и процессор имеют право как угодно перемещать выражения, пока эти условия выполняются. Если взять следующий код
int result = 0;
boolean done = false;
....
this.result = 1;
this.done = true;
то он имеет полное право превратиться в
this.done = true;
this.result = 1;
потому что все последующие выражения все равно увидят тот же самый результат. В это
примере другой поток, увидевший done = true, все еще может прочитать 0 из result. Однак
если объявить done как volatile, то запись в result обязана произойти до записи tru
в done, а чтение true - после его записи, и таким образом можно обеспечить гарантию видимости изменения в потоках-слушателях. Это не отменяет возможности того, что в result за этот период произойдет больше одной записи, только гарантирует то, что к моменту чтения из result в нем будет современное обновлению done или более позднее значение.
Обновление
Кроме всего вышеописанного, есть еще один забавный кейс. Java откровенно страдае
от хипов большого размера, точнее, от времени выполнения GC на таком хипе. Естественно
с этой проблемой пытаются бороться - с помощью GC, работающих в параллель с приложением
Одной из тактик в таком случае является эвакуация живых объектов из очищаемого региона
чтобы затем просто объявить его свободным для полной перезаписи. В этом случае в JV
одновременно могут жить две копии объекта (одна по старому адресу, и еще одна - эвакуируемая)
которые требуют сихронизации записей и чтения. К счастью для имплементаторов, JMM ничег
не обещает для обычных чтений, поэтому большинство операций можно освободить от синхронизации, и в один момент может сложиться ситуация, что все записи идут в один объект, а чтение производится из другого - до тех пор, пока доступ не синхронизован. Это, как и все вышеописанные примеры, находится в полном согласии с cache coherence, но допускает аномалии при работе приложения (и все по тем же причинам - cache coherence работает на уровне отдельных блоков памяти, JVM - объектов и полей). Этот абзац относится к Shenandoah GC, который ожидается в десятой джаве, но такие способы прострелить ногу можно смело ждать и в других ситуациях.
Ответ 3
Про false sharing очень правильно выше написали.
А вообще, есть простое правило, если есть один писатель и много читателей, volatile подходит идеально.
Если писателей много, нужны атомарные операции или иные синхронизирующие примитивы.
Комментариев нет:
Отправить комментарий