Страницы

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

вторник, 2 октября 2018 г.

Атомарные и неатомарные операции (java)

Как понять, какие операции являются атомарными, а какие неатомарными?
Вот что я нашла на Хабре
Операция в общей области памяти называется атомарной, если она завершается в один шаг относительно других потоков, имеющих доступ к этой памяти. Во время выполнения такой операции над переменной, ни один поток не может наблюдать изменение наполовину завершенным. Атомарная загрузка гарантирует, что переменная будет загружена целиком в один момент времени. Неатомарные операции не дают такой гарантии.
Т.е. как я поняла, атомарные операции - это достаточно мелкие, выполняющиеся "за один шаг относительно других потоков". Но что значит этот "шаг"?
Один шаг == одной машинной операции? Или чему-то другому? Как определить точно, какие операции относятся к атомарным, а какие к неатомарным?
P.S.: Я нашла похожий вопрос, но там речь идёт о C#...


Ответ

Как можно определить атомарность?
Атомарность операции чаще всего принято обозначать через ее признак неделимости: операция может либо примениться полностью, либо не примениться вообще. Хорошим примером будет запись значений в массив:
public class Curiousity { public volatile int[] array;
public void nonAtomic() { array = new int[1]; array[0] = 1; }
public void probablyAtomic() { array = new int[] { 1 }; } }
При использовании метода nonAtomic существует вероятность того, что какой-то поток обратится к array[0] в тот момент, когда array[0] не проинициализирован, и получит неожиданное значение. При использовании probablyAtomic (при том условии, что массив сначала заполняется, а уже потом присваивается - я сейчас не могу гарантировать, что в java это именно так, но представим, что это правило действует в рамках примера) такого быть не должно: array всегда содержит либо null, либо проинициализированный массив, но в array[0] не может содержаться что-то, кроме 1. Эта операция неделима, и она не может примениться наполовину, как это было с nonAtomic - только либо полностью, либо никак, и весь остальной код может спокойно ожидать, что в array будет либо null, либо значения, не прибегая к дополнительным проверкам.
Кроме того, под атомарностью операции зачастую подразумевают видимость ее результата всем участникам системы, к которой это относится (в данном случае - потокам); это логично, но, на мой взгляд, не является обязательным признаком атомарности.
Почему это важно?
Атомарность зачастую проистекает из бизнес-требований приложений: банковские транзакции должны применяться целиком, билеты на концерты заказываться сразу в том количестве, в котором были указаны, и т.д. Конкретно в том контексте, который разбирается (многопоточность в java), задачи более примитивны, но произрастают из тех же требований: например, если пишется веб-приложение, то разбирающий HTTP-запросы сервер должен иметь очередь входящих запросов с атомарным добавлением, иначе есть риск потери входящих запросов, а, следовательно, и деградация качества сервиса. Атомарные операции предоставляют гарантии (неделимости), и к ним нужно прибегать, когда эти гарантии необходимы.
Кроме того, атомарные операции линеаризуемы - грубо говоря, их выполнение можно разложить в одну линейную историю, в то время как просто операции могут производить граф историй, что в ряде случаев неприемлимо.
Почему примитивные операции не являются атомарными сами по себе? Так же было бы проще для всех.
Современные среды исполнения очень сложны и имеют на борту некислый ворох оптимизаций, которые можно сделать с кодом, но, в большинстве случаев, эти оптимизации нарушают гарантии. Так как большинство кода этих гарантий на самом деле не требует, оказалось проще выделить операции с конкретными гарантиями в отдельный класс, нежели наоборот. Чаще всего в пример приводят изменение порядка выражений - процессор и JVM имеют право выполнять выражения не в том порядке, в котором они были описаны в коде, до тех пор, пока программист не будет форсировать определенный порядок выполнения с помощью операций с конкретными гарантиями. Также можно привести пример (не уверен, правда, что формально корректный) с чтением значения из памяти:
thread #1: set x = 2 processor #1: save_cache(x, 2) processor #1: save_memory(x, 2) thread #2: set x = 1 processor #2: save_cache(x, 1) processor #2: save_memory(x, 1) thread #1: read x processor #1: read_cache(x) = 2 // в то время как х уже был обновлен значением 1 в thread #2
Здесь не используется т.н. single source of truth для того, чтобы управлять значением Х, поэтому возможны такие аномалии. Насколько понимаю, чтение и запись напрямую в память (или в память и в общий кэш процессоров) - это как раз то, что форсирует модификатор volatile (здесь могу быть неправ).
Конечно, оптимизированный код выполняется быстрее, но необходимые гарантии никогда не должны приноситься в жертву производительности кода
Это относится только к операциям связанным с установкой переменных и прочей процессорной сфере деятельности?
Нет. Любая операция может быть атомарной или неатомарной, например, классические реляционные базы данных гарантируют, что транзакция - которая может состоять из изменений данных на мегабайты - либо применится полностью, либо не будет применена. Процессорные инструкции здесь не имеют никакого отношения; операция может быть атомарной до тех пор, пока она является атомарной сама по себе или ее результат проявляется в виде другой атомарной операции (например, результат транзакции базы данных проявляется в записи в файл).
Кроме того, насколько понимаю, утверждение "инструкция не успела за один цикл - операция неатомарна" тоже неверно, потому что есть некоторые специализированные инструкции, и никто не мешает атомарно устанавливать какое-либо значение в памяти на входе в защищенный блок и снимать его по выходу.
Любая ли операция может быть атомарной?
Нет. Мне очень сильно не хватает квалификации для корректных формулировок, но, насколько понимаю, любая операция, подразумевающая два и более внешних эффекта (сайд-эффекта), не может быть атомарной по определению. Под сайд-эффектом в первую очередь подразумевается взаимодействие с какой-то внешней системой (будь то файловая система или внешнее API), но даже два выражения установки переменных внутри synchronized-блока нельзя признать атомарной операцией, пока одно из них может выкинуть исключение - а это, с учетом OutOfMemoryError и прочих возможных исходов, может быть вообще невозможно.
У меня операция с двумя и более сайд-эффектами. Могу ли я все-таки что-нибудь с этим сделать?
Да, можно создать систему с гарантией применения всех операций, но с условием, что любой сайд-эффект может быть вызван неограниченное число раз. Вы можете создать журналируемую систему, которая атомарно записывает запланированные операции, регулярно сверяется с журналом и выполняет то, что еще не применено. Это можно представить следующим образом:
client: journal.push {withdrawMoney {card=41111111, cvc=123}, reserveTicket {concert=123}, sendEmail {address=nobody@localhost}} client: <журнал подтвердил получение и запись задания> journal: process withdrawMoney journal: markCompleted withdrawMoney journal: process reserveTicket journal: <умирает, не успев записать выполнение reserveTicket> journal: <восстанавливается> journal: process reserveTicket # сайд-эффект вызывается еще раз, но только в случае некорректной работы journal: markCompleted reserveTicket journal: process sendEmail journal: markCompleted sendEmail
Это обеспечивает прогресс алгоритма, но снимает все обязательства по временным рамкам (с которыми, формально говоря, и без того не все в порядке). В случае, если операции идемпотентны, подобная система будет рано или поздно приходить к требуемому состоянию без каких-либо заметных отличий от ожидаемого (за исключением времени выполнения).
Как все-таки определить атомарность операций в java?
Первичный источник правды в этом случае - это Java Memory Model, которая определяет, какие допущения и гарантии применяются к коду в JVM. Java Memory Model, впрочем, довольно сложна для понимания и покрывает значительно большую сферу операций, нежели сфера атомарных операций, поэтому в контексте этого вопроса достаточно знать, что модификатор volatile обеспечивает атомарное чтение и запись, а классы Atomic* позволяют производить compare-and-swap операции, чтобы атомарно менять значения, не боясь, что между чтением и записью придет еще одна чья-то запись, а в комментариях ниже на момент прочтения наверняка добавили еще что-то, что я забыл.

Комментариев нет:

Отправить комментарий