Re: Не делимые операции потоков
От: boluba  
Дата: 15.06.06 03:57
Оценка: 54 (13)
Начну с того, что (как мне кажется) понимаю, а потом задам вопросы, если позволите .

Lucker пишет:
настаиваю на не путать атомарность и потокобезопасность!


Я не знаю, есть ли классическое определение атомарности (наверняка есть), но я всегда думал о нем так: если операция атомарна, то, какой бы продолжительной она не была, результат операции можно точно предсказать в момент ее начала. Атомарность гарантируется средой исполнения (ОС, СУБД, VM, ect.).
Например, атомарной может быть операция записи массива байт в файл. Она занимает какое-то время, но, используя ее, я могу быть уверен, что во время записи массива другие потоки не смогут писать в тот же файл. Иными словами, никакой другой процесс не сможет повлиять на результат операции. А значит в начале выполнения операции я точно знаю результат (возможность аппаратного сбоя не рассматривается). При этом ОС может отдать процессор другому процесу, прервав мой в середине операции записи (Пример немного не реальный, но от него этого и не требуется ).

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

Теперь по теме наконец (если еще кто-то читает):
— JSL,SE. Ищем упоминание атомарности и потокобезопасности и все, что на это похоже.

JLS, SE:17.1 Terminology and Framework
A variable is any location within a program that may be stored into. <skip> Variables are kept in a main memory that is shared by all threads. <skip> Every thread has a working memory in which it keeps its own working copy of variables that it must use or assign. As the thread executes a program, it operates on these working copies. The main memory contains the master copy of every variable. <skip> the verbs use, assign, load, store, lock, and unlock name actions that a thread can perform. The verbs read, write, lock, and unlock name actions that the main memory subsystem can perform. Each of these actions is atomic (indivisible).

Получается, согласно спецификации, есть несколько атомарных операций, которые не являются ни частью языка Java, ни байткодами для JVM (однако, информация о них входит в обе спецификации). Важно, что хоть что-то атомарное есть.
Предположим есть код:
class Sample {
    int a = 1, b = 2;
    void hither() {
        a = b;
    }
    void yon() {
        b = a;
    }
}

(Это все из той же главы 17)

Поток, выполняя метод hither, должен сделать следующее:
1. read b — memory subsystem, читает значение "b" из общей памяти
2. load b — current thread, записывает значение "b" в свою локальную память
3. use b — current thread, ???
4. assign a — current thread, ???
5. store a — current thread, записывает значение "a" в свою локальную память
6. write a — memory subsystem, записать значение "a" в общую память

Насколько я понимаю, use означает любое использование, кроме присвоения результата. Подобным образом можно описать и инкремент:
class Sample {
    int a = 1;
    void hither() {
        a++;
    }
}


Вот так:
1. read a — memory subsystem, читает значение "a" из общей памяти
2. load a — current thread, записывает значение "a" в свою локальную память
3. use a — current thread, здесь это инкремент.
4. assign a — current thread, ???
5. store a — current thread, записывает значение "a" в свою локальную память
6. write a — memory subsystem, записать значение "a" в общую память

Т.е. шесть атомарных метаопераций (назовем их так), но атомарности инкремента нет. Если представить себе поток, который выполнил шаг 3 и был прерван. И второй поток, который выполняет этот же код и тоже дошел до шага 3. Оба скопировали из общей памяти значение переменной "a" равное "1". Дальше не важно, какой поток пройдет все 6 шагов первым. Первый поток сохранит значение "2" в общую память. Второй перезапишет результат первого своим результатом (тоже равным "2"). Этот результат подтверждается тестом, предложенным dshe.

Важно отметить тут, что шаг 6 может не выполняться сразу после шага 5. Ведь результат может быть использован дальше.

class Sample {
    int a = 1, b = 7;
    void hither() {
        a++;
                b = b + a;
    }
}


Уф... Лирическое отступление:
Больше года назад (когда текущей была JLS, SE) в нескольких форумах и переписке с разными людьми я задавал вопросы, касающиеся последовательности выполнения, оптимизации во время выполнения, etc. Я считал (и считаю) главу 17 JLS SE слишком сложной и непонятно. Иногда даже противоречивой (Меня больше всего интересовали volatile переменные. О них ниже ). Как выяснилось, Sun признала, что их реализация JRE нарушает спецификацию в отношении volatile переменных. (Я где-то видел что-то типа теста, который это подтверждал, но сам я не проверял, так что — за что купил, за то продаю ).


Теперь по потокобезопасности.
class Sample {
    int a = 1;
    void hither() {
        a++;                
    }
}


Если бы инкремент был атомарной операцией, то код был бы потокобезопасным (в свете всего сказанного выше). Но это не так (тест, предложенный dshe).


Lucker пишет:
A>Я может чего не так понимаю ...

ну это всего лишь значит что при выполнении таких операций в многопоточной среде ты не получишь "неожиданного" результата, как например в случае с long-ами, где старшие байты могут быть изменены в одном потоке, младшие в другом и в результате получится не рыба не мясо.


Поддерживаю Lucker. Я это понимаю так: Речь идет об метооперации "use". Допустим у нас есть код:
class Sample {
    int a = 1, b = 7, c = 1;
    void hither() {
                c = b + a;
    }
}


Согласно спецификации, это выглядит так:
1. read a — memory subsystem, читает значение "a" из общей памяти
2. load a — current thread, записывает значение "a" в свою локальную память
3. read b — memory subsystem, читает значение "b" из общей памяти
4. load b — current thread, записывает значение "b" в свою локальную память
5. use a,b — current thread, здесь это инкремент.
6. assign c — current thread, ???
7. store c — current thread, записывает значение "a" в свою локальную память
8. write c — memory subsystem, записать значение "a" в общую память

Вот шаг 5 — это сложение, которое атомарное . Повторюсь, это лишь моя версия событий. Так я понимаю спецификацию.

Первое замечание: здравый смысл говорит мне, что шаги 1 и 2 нельзя менять местами, но шаги 1 и 2 могут быть исполнены после шагов 3 и 4.

1. read b — memory subsystem, читает значение "b" из общей памяти
2. load b — current thread, записывает значение "b" в свою локальную память
3. read a — memory subsystem, читает значение "a" из общей памяти
4. load a — current thread, записывает значение "a" в свою локальную память

Эти две группы метаопераций не влияют друг на друга, не так ли?

Второе замечание: перед use не обязательно будет read и load. А после assign не всегда store и write. Т.е. копия переменной в рабочей области памяти текущего процесса не обязана синхронизироваться с мастер-копией после и перед каждой операции(ей). Как раз это позволяет хранить переменный потока в регистрах, например.

Теперь пара слов о volatile переменных. Что они могут и чего не могут. Выжимка из 17.7 Rules for Volatile Variables:
— Перед каждым использованием (use) значения переменных синхронизируются с мастер-копиями (read, load).
— После каждого использования (assign) значения переменных синхронизируются с мастер-копией (store, write).
— Последовательность синхронизации volatile переменных с мастер-копиями не может быть изменена. (Это относиться к одному потоку)


JLS, SE:
The load, store, read, and write actions on volatile variables are atomic, even if the type of the variable is double or long.


Делает ли модификатор volatile операции с переменной атомарным? Нет. Потокобезопасным? Тоже нет.

korostoff пишет:
Вилимо потому что последовательность
загрузка volatile n1 из master memory, инкремент, сохранение нового значения n1 в master memory
само по себе не атомарно.


В точку. Это тоже очень интересная тема. Многие, с кем я общался по проблеме volatile переменных (в том числе и я по началу) считали, что volatile — это что-то вроде "монитора для примитивов" . Т.е. я предполагал, что объявив переменную с модификатором volatile:
class TestVolatile {
   private volatile int foo = 1;

   public void up() {
     foo++;
   }
}


я получу такое поведение:

class TestVolatile {
   private int foo = 1;

   public synchronized void up() {
     foo++;
   }
}

Два потока вызывают метод. Тут все просто. Потоки синхронизируются на мониторе экземпляра TestVolatile. Многие считают, что первая версия (с использованием volatile) является способом "синхронизации на мониторе примитива". С примитивами не связаны мониторы и такая версия событий, что называется, reasonable. Но увы, это не так.

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


Пытаясь понять JLS, SE, я подписался на рассылку и прочел кучу материалов:
http://www.cs.umd.edu/~pugh/java/memoryModel/
http://www.jcp.org/en/jsr/detail?id=133
https://javamemorymodel.dev.java.net/

К сожалению, в деталях JMM JLS3 я не разбирался. Я прочитал jsr в надежде, что она станет проще и понятнее . Что могу сказать — она точно изменилась...


А теперь вопрос:
— Все что я писал вполне может оказаться бредом. Пожалуйста, поправьте меня.
— Поразмышляв еще немного над всем этим, можно прийти к выводу, что JLS не запрещает JVM переключать процессы в середине исполнения очередного байткода. Справедливо ли это для JLS3?
— Все эти сложности со спецификацией направлены на то, чтобы дать разработчикам JVM как можно больше свободы при реализации многопороцесности. Это понятно. А вот кто-нибудь может авторитетно заявить, как реализована многопроцесность в JVM для WIN32, например? Т.е. создание потока выполнения создает процесс или поток? Т.е. насколько это дорогая операция? А как с этим в Linux например?

Спасибо всем, кто дочитал . Большое спасибо тем, кто ответит !
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.