Страницы

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

вторник, 26 ноября 2019 г.

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


Как понять, какие операции являются атомарными, а какие неатомарными?

Вот что я нашла на Хабре:


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


Т.е. как я поняла, атомарные операции - это достаточно мелкие, выполняющиеся "з
один шаг относительно других потоков". Но что значит этот "шаг"? 

Один шаг == одной машинной операции? Или чему-то другому? Как определить точно, какие операции относятся к атомарным, а какие к неатомарным?

P.S.: Я нашла похожий вопрос, но там речь идёт о C#...  
    


Ответы

Ответ 1



Как можно определить атомарность? Атомарность операции чаще всего принято обозначать через ее признак неделимости операция может либо примениться полностью, либо не примениться вообще. Хорошим примером будет запись значений в массив: 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 // в то время как х уже был обновлен значением в 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 операции, чтобы атомарно менять значения, не боясь, что между чтением и записью придет еще одна чья-то запись, а в комментариях ниже на момент прочтения наверняка добавили еще что-то, что я забыл.

Ответ 2



Как понять, какие операции являются атомарными, а какие неатомарными? Рискуя на себя навлечь обвинения в сексизме, таки не удержусь и приведу пример атомарно операции: беременность - операция строго атомарная, всегда есть один и только один отец (всякие генные ухищрения вынесем за скобки). И наоборот пример неатомарной операции: воспитание ребенка - увы операция неатомарная ребенок есть к сожалению субъект множества различных несинхронизированных операций над неокрепшей душой ребенка: мама, папа, бабушка, дедушка, зомбоящик, детсад, школа, друзья, подруги и т.д. по списку.

Ответ 3



Попробую объяснить. Могу ошибаться. Есть java, исходники компилируются в байткод. Байткод во время выполнения программ преобразуется в машинный код. Одна инструкция/команда в байткоде может преобразоватьс в несколько инструкций машинного кода. В этом и заключается проблема атомарности. Процессо не может за раз выполнить одну команду, написанную на языке высокого уровня: он выполняет машинный код, содержащий последовательность команд. Следовательно, если разные процессоры выполняют манипуляции над одними и теми же данными, то различные инструкции процессоров могут чередоваться. Приведу пример: Есть глобальная переменная: public volatile int value = 0; first-thread { value++ } second-thread{ value++ } Инкрементирование переменной не является атомарной операцией: оно требует, как минимум, три инструкции: прочитать данные увеличить на единицу записать данные Соответственно, два потока должны выполнить эту последовательность, но порядок и выполнения между ними не определен. Из-за этого и могут возникнуть ситуации вроде следующей: Первый поток прочел данные Второй поток прочел данные Первый поток увеличил значение на 1 Второй поток увеличил значение на 1 Второй поток записал значение Первый поток записал значение В результате имеем результат 1, а не 2 как ожидалось. Чтобы такого не происходило, используют либо синхронизацию, либо атомарные примитивы из пакета java.util.concurrent

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

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