Страницы

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

Показаны сообщения с ярлыком атомарность. Показать все сообщения
Показаны сообщения с ярлыком атомарность. Показать все сообщения

понедельник, 6 января 2020 г.

Атомарность операции получения экземпляра класса на x64 приложении

#c_sharp #многопоточность #атомарность


Атомарность присваивания можно добиться с помощью System.Threading.Interlocked

Interlocked.Exchange(ref toItem, fromItem);


А вот как атомарно получить экземпляр класса атомарно, ведь у System.Threading.Interlocked
нет метода Read(ref object), есть только Read(ref long)?

TObject obj = somethingObject; 
//Сомнение атомарности. 
//Поскольку передача указателя x64 не умещается в такт процессора, 
//и не гарантирует атомарности.


просьба не приводить примеры с lock, т.к. мне они не интересны.
    


Ответы

Ответ 1



Операция чтения ссылки на объект всегда атомарная. Это следствие базового принципа .NET: если ваш код не содержит вызовов unsafe - вы никак не можете получить некорректную ссылку на объект. Как следствие, всегда атомарными являются чтение int, указателей и IntPtr. Более того, чтение long тоже атомарно на 64-битной системе (найденный вами метод Interlocked.Read(ref long) предназначен для платформ x86 или AnyCPU) Однако, надо понимать что Interlocked-операции - это не только атомарность, это еще и барьеры памяти. Поэтому читать разделяемую переменную как TObject obj = somethingObject нельзя - это чтение может дать устаревшие данные. Для чтения нужно использовать метод Volatile.Read: TObject obj = Volatile.Read(ref somethingObject);

суббота, 4 января 2020 г.

Неатомарные операции С++

#cpp #терминология #атомарность


Какие операции называются неатомарными?
Если можно с примером.
    


Ответы

Ответ 1



Грубо - которые могут оказаться прерванными другим потоком. Примеры? Ну, например if (a) { f(a); } Пока вы считали a, которое равно true, и собрались выполнять f(a), другой поток уже сделал a равным false :) Или даже такая экзотика - в одном потоке вы присваиваете значение переменной из двух слов. Первое слово записалось, прерывание, другой поток читает из памяти нечто полузаписанное. Работает, записывает что-то свое. Тут снова управление возвращается к первому потоку, и он дописывает вторую половину нашей переменной...

Ответ 2



В C++ атомарными операциями являются лишь операции над std::atomic, все остальные операции неатомарны. При соблюдении определенных условий, на определённой архитектуре, атомарными могут быть операции и над «простыми» объектами, но стандартом C++ это не гарантируется. К примеру, есть у Вас такой код: int64_t var{}; var = -1; Если мы запустим его на 64-х битной архитектуре (amd64, к примеру) то наверняка операция var = -1 будет атомарной, т.к. шина позволяет записать за один раз все 64 бита, т.е. у нас будет один mov в ассемблере. Если же мы соберём этот код для 32-х битного процессора (x86, к примеру), то эта операция перестанет быть атомарной и наверняка будет состоять из двух последовательных mov, т.к. за одну неделимую операцию нельзя в 32-х битную шину засунуть 64 бита данных. Правда, в процессорах есть достаточно средств, чтобы превращать несколько операций в одну атомарную, но эти средства будут как раз использоваться с std::atomic. Если же средств процессора для реализации std::atomic недостаточно, то выходят на более высокий уровень — блокировка мьютексов и прочее. С точки зрения процессора такие операции не могут считаться атомарными, но с точки зрения приложения они будут атомарны, т.к. ни в какой момент времени программа на сможет прочитать частично записанное значение.

Ответ 3



Вообще, что атомарно, а что нет надо искать в описании компилятора, для конкретного процессора, например для некоторых DSP присвоение является атомарным, но обычно даже оно не атомарно, для надёжности лучше пользоваться чем то типа std::atomic

вторник, 24 декабря 2019 г.

Изменение атомарного значения из нескольких потоков в Java

#java #многопоточность #атомарность


Здравствуйте.
Помогите, пожалуйста, разобраться со следующей задачей:


  Воспользуйтесь классом LongAccumulator для вычисления максимального и
  минимального накапливаемых элементов.


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

import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAccumulator;

public class Main {
    public static LongAccumulator maxValue = new LongAccumulator(Math::max, Integer.MIN_VALUE);
    public static LongAccumulator minValue = new LongAccumulator(Math::min, Integer.MAX_VALUE);

    public static void main(String[] args){
        Random random = new Random();
//        Executor executor = Executors.newCachedThreadPool();
        Executor executor = Executors.newFixedThreadPool(1);
        for (int i = 1; i <= 1000; i++) {
            int taskId = i;
            Runnable task = () -> {
                for (int k = 1; k <= 100_000; k++){
                    int value = random.nextInt(1_000_000);
                    maxValue.accumulate(value);
                    minValue.accumulate(value);
                }
                System.out.println(taskId + ": min " + minValue.get() + ": max "
+ maxValue.get());
            };
            executor.execute(task);
        }
    }
}


Всё работает, но не так, как я ожидал. Если все задачи поместить в один поток (см.
строку Executor executor = Executors.newFixedThreadPool(1);), то всё отрабатывает более-менее
быстро.
Если же вместо этой строки подставить Executor executor = Executors.newCachedThreadPool();,
или поставить несколько потоков: Executor executor = Executors.newFixedThreadPool(5);,
то код работает в несколько раз дольше.
Собственно, вопрос состоит в том, почему это происходит и как сделать так, чтобы
разделение задач на потоки не увеличивало время работы программы, а уменьшало.

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

import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAccumulator;

public class Main {
    public static LongAccumulator maxValue = new LongAccumulator(Math::max, Integer.MIN_VALUE);
    public static LongAccumulator minValue = new LongAccumulator(Math::min, Integer.MAX_VALUE);

    public static void main(String[] args){
        Executor executor = Executors.newCachedThreadPool();
        Runnable task; // вынес объявление из цикла ниже
        for (int i = 1; i <= 1000; i++) {
            Random random = new Random();  // для каждой задачи создаётся свой Random
            int taskId = i;
            task = () -> {
                for (int k = 1; k <= 100_000; k++){
                    int value = random.nextInt(1_000_000);
                    maxValue.accumulate(value);
                    minValue.accumulate(value);
                }
                System.out.println(taskId + ": min " + minValue.get() + ": max "
+ maxValue.get());
            };
            executor.execute(task);
        }
    }
}

    


Ответы

Ответ 1



Вынесите объявление Runnable класса из цикла. 1000 потоков (для cachedPool будет именнно так) не даст эффективности - jvm будет между ними переключаться постоянно, а т.к. тело задачи довольно "быстрое" то и эффекта это особо не даст. Поставьте кол-во потоков равным количеству ядер вашего CPU. Random для каждой задачи используйте свой

вторник, 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

вторник, 5 февраля 2019 г.

Атомарность операции получения экземпляра класса на x64 приложении

Атомарность присваивания можно добиться с помощью System.Threading.Interlocked
Interlocked.Exchange(ref toItem, fromItem);
А вот как атомарно получить экземпляр класса атомарно, ведь у System.Threading.Interlocked нет метода Read(ref object), есть только Read(ref long)?
TObject obj = somethingObject; //Сомнение атомарности. //Поскольку передача указателя x64 не умещается в такт процессора, //и не гарантирует атомарности.
просьба не приводить примеры с lock, т.к. мне они не интересны.


Ответ

Операция чтения ссылки на объект всегда атомарная. Это следствие базового принципа .NET: если ваш код не содержит вызовов unsafe - вы никак не можете получить некорректную ссылку на объект.
Как следствие, всегда атомарными являются чтение int, указателей и IntPtr. Более того, чтение long тоже атомарно на 64-битной системе (найденный вами метод Interlocked.Read(ref long) предназначен для платформ x86 или AnyCPU)
Однако, надо понимать что Interlocked-операции - это не только атомарность, это еще и барьеры памяти. Поэтому читать разделяемую переменную как TObject obj = somethingObject нельзя - это чтение может дать устаревшие данные.
Для чтения нужно использовать метод Volatile.Read
TObject obj = Volatile.Read(ref somethingObject);

понедельник, 28 января 2019 г.

Неатомарные операции С++

Какие операции называются неатомарными? Если можно с примером.


Ответ

Грубо - которые могут оказаться прерванными другим потоком.
Примеры? Ну, например
if (a) { f(a); }
Пока вы считали a, которое равно true, и собрались выполнять f(a), другой поток уже сделал a равным false :)
Или даже такая экзотика - в одном потоке вы присваиваете значение переменной из двух слов. Первое слово записалось, прерывание, другой поток читает из памяти нечто полузаписанное. Работает, записывает что-то свое. Тут снова управление возвращается к первому потоку, и он дописывает вторую половину нашей переменной...

понедельник, 26 ноября 2018 г.

Изменение атомарного значения из нескольких потоков в Java

Здравствуйте. Помогите, пожалуйста, разобраться со следующей задачей:
Воспользуйтесь классом LongAccumulator для вычисления максимального и минимального накапливаемых элементов.
Подразумевается, что должно быть несколько потоков, а вид генерации элементов произвольный. Вот мой код:
import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.LongAccumulator;
public class Main { public static LongAccumulator maxValue = new LongAccumulator(Math::max, Integer.MIN_VALUE); public static LongAccumulator minValue = new LongAccumulator(Math::min, Integer.MAX_VALUE);
public static void main(String[] args){ Random random = new Random(); // Executor executor = Executors.newCachedThreadPool(); Executor executor = Executors.newFixedThreadPool(1); for (int i = 1; i <= 1000; i++) { int taskId = i; Runnable task = () -> { for (int k = 1; k <= 100_000; k++){ int value = random.nextInt(1_000_000); maxValue.accumulate(value); minValue.accumulate(value); } System.out.println(taskId + ": min " + minValue.get() + ": max " + maxValue.get()); }; executor.execute(task); } } }
Всё работает, но не так, как я ожидал. Если все задачи поместить в один поток (см. строку Executor executor = Executors.newFixedThreadPool(1);), то всё отрабатывает более-менее быстро. Если же вместо этой строки подставить Executor executor = Executors.newCachedThreadPool();, или поставить несколько потоков: Executor executor = Executors.newFixedThreadPool(5);, то код работает в несколько раз дольше. Собственно, вопрос состоит в том, почему это происходит и как сделать так, чтобы разделение задач на потоки не увеличивало время работы программы, а уменьшало.
UPD. По совету, приведённому в комментариях, изменил код следующим образом, и программа стала работать быстрей в многопоточном режиме:
import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.LongAccumulator;
public class Main { public static LongAccumulator maxValue = new LongAccumulator(Math::max, Integer.MIN_VALUE); public static LongAccumulator minValue = new LongAccumulator(Math::min, Integer.MAX_VALUE);
public static void main(String[] args){ Executor executor = Executors.newCachedThreadPool(); Runnable task; // вынес объявление из цикла ниже for (int i = 1; i <= 1000; i++) { Random random = new Random(); // для каждой задачи создаётся свой Random int taskId = i; task = () -> { for (int k = 1; k <= 100_000; k++){ int value = random.nextInt(1_000_000); maxValue.accumulate(value); minValue.accumulate(value); } System.out.println(taskId + ": min " + minValue.get() + ": max " + maxValue.get()); }; executor.execute(task); } } }


Ответ

Вынесите объявление Runnable класса из цикла. 1000 потоков (для cachedPool будет именнно так) не даст эффективности - jvm будет между ними переключаться постоянно, а т.к. тело задачи довольно "быстрое" то и эффекта это особо не даст. Поставьте кол-во потоков равным количеству ядер вашего CPU. Random для каждой задачи используйте свой

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