Алгоритм Петерсона в Java?

Есть ли пример реализации алгоритма Петерсона для взаимного исключения в Java?


person Gabriel Ščerbák    schedule 26.05.2010    source источник
comment
Пожалуйста, не стесняйтесь публиковать свои ответы, я ищу лучший!   -  person Gabriel Ščerbák    schedule 26.05.2010
comment
Вы имеете в виду алгоритм Петерсона для реализации взаимного исключения только с общей памятью с сильными гипотезами? Я сомневаюсь, что модель памяти Java (или любого другого языка, достаточно современного, чтобы явно указать модель памяти) позволяет алгоритму Петерсона работать... И если вы собираетесь использовать явные барьеры памяти, почему бы не использовать инструкции синхронизации, предоставляемые языком?   -  person Pascal Cuoq    schedule 26.05.2010
comment
@Pascal Cuoq Я не уверен, можете ли вы указать мне оригинальную статью об этом алгоритме, чтобы я мог проверить? Каким предварительным условиям должна удовлетворять семантика модели памяти, чтобы гарантировать корректность алгоритма? Причина отказа от использования механизмов параллелизма и синхронизации, предоставляемых Java, заключается в том, что я просто пытаюсь понять алгоритм Петерсона, а не параллельное программирование на Java.   -  person Gabriel Ščerbák    schedule 26.05.2010
comment
@Gabriel Алгоритм настолько короткий (исходная статья известна тем, что состоит всего из 2 страниц), что полностью находится на странице Википедии: en.wikipedia.org/wiki/Peterson%27s_algorithm . См. также раздел «Примечание».   -  person Pascal Cuoq    schedule 26.05.2010
comment
@Pascal Cuoq Я прочитал статью, но не смог найти упоминания о сильных гипотезах. Итак, если я правильно понял, проблема заключается в возможности переупорядочивания доступа к памяти?   -  person Gabriel Ščerbák    schedule 26.05.2010
comment
@Gabriel Вы правы, проблема в переупорядочении доступа к памяти. Статья предшествует оптимизированным реализациям многопроцессорности/многоядерности. Разработчики DEC Alpha первыми ослабили идею единой общей памяти, последовательно просматриваемой всеми процессорами/ядрами/потоками. Эта единственная непротиворечивая гипотеза общей памяти подразумевается (но явно выдвигается) в статье.   -  person Pascal Cuoq    schedule 26.05.2010
comment
@Pascal Cuoq ах, теперь я понимаю, о чем ты говоришь, спасибо :)   -  person Gabriel Ščerbák    schedule 26.05.2010


Ответы (5)


Здесь никто не предоставил правильную/безопасную реализацию этого алгоритма на Java. Я не уверен, как должно работать решение Джона В., поскольку в нем отсутствуют части (а именно объявления ThreadLocals и объяснение того, что должно быть в его массиве — примитив booleans не имеет get() и set()).

Глава 17 Спецификации языка Java объясняет Модель памяти Java. Особый интерес представляет Раздел 17.4.5, который описывает порядок происходит до. Об этом довольно легко думать в рамках одного потока. Рассмотрим фрагмент:

int x, y, z, w;
x = 0;
y = 5;

z = x;
w = y;

Все согласятся, что в конце этого фрагмента x и z равны 0, а y и w равны 5. Игнорируя объявления, у нас есть шесть действий:

  1. Написать x
  2. Пишите y
  3. Чтение от x
  4. Пишите z
  5. Чтение от y
  6. Пишите w

Поскольку все они появляются в одном потоке, JLS говорит, что эти операции чтения и записи гарантированно демонстрируют этот порядок: каждое действие n выше (поскольку действия находятся в одном потоке) имеет событие «происходит до». связь со всеми действиями m, m > n.

Но как насчет разных потоков? При обычном доступе к полям между потоками не устанавливаются отношения "происходит до". Это означает, что поток A может увеличивать общую переменную, а поток B может читать эту переменную, но не видеть новое значение< /эм>. При выполнении программы в JVM распространение записи потока A могло быть переупорядочено так, чтобы оно происходило после чтения потока B.

На самом деле, поток A может записывать в переменную x, а затем в переменную y, устанавливая связь между этими двумя действиями в потоке A. Но поток B может читать x и y, и B имеет право получить новое значение y до появления нового значения x. Спецификация говорит:

Более конкретно, если два действия имеют общую связь «происходит до», они не обязательно должны казаться произошедшими в этом порядке для любого кода, с которым они не имеют общей связи «произошло до».

Как это исправить? Для обычного доступа к полям достаточно ключевого слова volatile:

Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими операциями чтения v любым потоком (где последующее определяется в соответствии с порядком синхронизации).

синхронизируется с является более сильным условием, чем "происходит-до", и, поскольку "происходит-до" является транзитивным, если поток A хочет, чтобы поток B видел свои записи в x и y, ему просто нужно записать в volatile переменная z после записи x и y. Поток B должен прочитать из z перед чтением x и y, и он гарантированно увидит новые значения x и y.

В решении Габриэля мы видим такую ​​закономерность: запись происходит в in, которая не будет видна другим потокам, но затем происходит запись в turn, так что другие потоки гарантированно увидят обе записи, пока они читают turn первыми.

К сожалению, условное выражение цикла while является обратным: чтобы гарантировать, что поток не увидит устаревшие данные для in, цикл while должен сначала прочитать из turn:

    // ...
    while (turn == other() && in[other()]) {
        // ...

Имея в виду это исправление, большая часть остального решения в порядке: в критической секции нас не волнует устаревание данных, потому что мы находимся в критической секции! Единственный другой недостаток возникает в конце: Runnable устанавливает in[id] в новое значение и завершает работу. Будет ли гарантировано, что другой поток увидит новое значение in[id]? Спецификация говорит нет:

Последнее действие в потоке T1 синхронизируется с любым действием в другом потоке T2, обнаружившем завершение T1. T2 может выполнить это, вызвав T1.isAlive() или T1.join().

Итак, как мы это исправим? Просто добавьте еще одну запись в turn в конце метода:

    // ...
    in[id] = false;
    turn = other();
}
// ...

Поскольку мы переупорядочили цикл while, другой поток гарантированно увидит новое ложное значение in[id], потому что запись в in[id] происходит до того, как произойдет запись в turn, до того, как произойдет чтение из turn, до того, как произойдет чтение из in[id].

Излишне говорить, что без метрики тонны комментариев этот метод хрупок, и кто-то может прийти и что-то изменить и тонко нарушить правильность. Простого объявления массива volatile недостаточно: как объясняется в этом поток Билла Пью (одного из ведущих исследователей модель памяти Java), объявление массива volatile делает обновления массива reference видимыми для других потоков. Обновления элементов массива не обязательно видны (отсюда и все циклы, через которые нам только что пришлось пройти, используя другую переменную volatile для защиты доступа к элементам массива).

Если вы хотите, чтобы ваш код был ясным и лаконичным, оставьте его таким, какой он есть, и измените in на AtomicIntegerArray (используйте 0 для false, 1 для true; AtomicBooleanArray не существует). Этот класс действует как массив, все элементы которого равны volatile, и поэтому прекрасно решит все наши проблемы. В качестве альтернативы вы можете просто объявить две изменчивые переменные, boolean in0 и boolean in1, и обновить их вместо использования логического массива.

person jasonmp85    schedule 26.05.2010
comment
О, спасибо, это, наверное, лучший ответ, который я когда-либо получал на StackOverflow. Это очень интересно и вдохновляюще, я думаю, я посмотрю на некоторые параллельные Java API. - person Gabriel Ščerbák; 27.05.2010
comment
Иногда книги настолько хороши, что вызывают прозвища. Книга PickAx, K&R C и Книга динозавров - вот лишь некоторые из них. Книга поездов (javaconcurrencyinpractice.com) не является исключением. Он охватывает основы модели памяти и параллелизма, а затем очень подробно рассказывает о java.util.concurrent, оставаясь в высшей степени читабельным. Если вы хотите узнать больше о замечательных инструментах в java.util.concurrent, возьмите его или одолжите у друга. - person jasonmp85; 27.05.2010
comment
Этот ответ фантастический. Кто на земле проголосовал за это и почему? ИМХО, грубо минусовать без какого-либо комментария. - person Dave Dopson; 13.07.2012

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

Например, вам может пригодиться эта глава книги, посвященная условиям гонки и взаимному исключению в Java: http://java.sun.com/developer/Books/performance2/chap3.pdf

В частности:

Java предоставляет встроенную поддержку ожидания этого «изменения состояния» через понятие условия. Однако термин «условие» является немного неправильным, поскольку от пользователя полностью зависит, возникло ли условие на самом деле. Кроме того, условие не обязательно должно быть конкретно истинным или ложным. Чтобы использовать условия, необходимо ознакомиться с тремя ключевыми методами класса Object:

• wait(): этот метод используется для ожидания условия. Он вызывается, когда в настоящее время удерживается блокировка для определенного (общего) объекта.

• notify(): этот метод используется для уведомления отдельного потока о том, что условие (возможно) изменилось. Опять же, этот метод вызывается, когда в данный момент удерживается блокировка для определенного объекта. В результате этого вызова может быть пробужден только один поток.

• notifyAll(): этот метод используется для уведомления нескольких потоков о том, что условие (возможно) изменилось. Все потоки, работающие во время вызова этого метода, будут уведомлены.

person Daniel Renshaw    schedule 26.05.2010
comment
Как я уже упоминал в комментарии к вопросу, это относится к алгоритму, поэтому вы не ответили на мой вопрос, однако я ценю усилия и хорошие цитаты, спасибо :) - person Gabriel Ščerbák; 26.05.2010
comment
Люди, изучающие компьютерные науки, должны знать, как работает алгоритм блокировки, не прибегая к языковым функциям высокого уровня. Такие классы, как распределенные вычисления или операционная система, требуют от нас знания того, как работает механизм блокировки, и нам часто требуется реализовать такие алгоритмы на любом языке. - person AlxDroidDev; 19.03.2017

Сам я не смог найти его в сети, поэтому решил попробовать написать:


public class Peterson implements Runnable {

    private static boolean[] in = { false, false };
    private static volatile int turn = -1;

    public static void main(String[] args) {
        new Thread(new Peterson(0), "Thread - 0").start();
        new Thread(new Peterson(1), "Thread - 1").start();
    }

    private final int id;

    public Peterson(int i) {
        id = i;
    }

    private int other() {
        return id == 0 ? 1 : 0;
    }

    @Override
    public void run() {
        in[id] = true;
        turn = other();
        while (in[other()] && turn == other()) {
            System.out.println("[" + id + "] - Waiting...");
        }
        System.out.println("[" + id + "] - Working ("
                + ((!in[other()]) ? "other done" : "my turn") + ")");
        in[id] = false;
    }
}

Не стесняйтесь комментировать, это будет оценено :)

person Gabriel Ščerbák    schedule 26.05.2010
comment
Вам следует ознакомиться с соответствующими разделами модели памяти Java (is.gd/cpKfw). Запись в элементы массива не устанавливает никаких отношений «происходит до», поэтому нет гарантии, что Thread1 увидит, что параметр Thread0 в [0] равен true. Он даже не работает с volatile (is.gd/cpKpY). Возможно, попробуйте переписать это, используя AtomicIntegerArray, как в (is.gd/cpKrf), используя 0 и 1 вместо true и ложный. - person jasonmp85; 26.05.2010

Вы действительно должны проверить книгу Искусство многопроцессорного программирования. Он очень подробно рассматривает различные реализации блокировок (как спиновых, так и блокирующих). Он также рассматривает различные другие типы параллельных алгоритмов (например, список пропуска). Вот фрагмент из его книги об алгоритме блокировки Петерсона.

Class Peterson implements Lock{
   private final boolean []flag = new boolean[2];
   private volatile int victim;

   public void lock(){
        int i = ThreadID.get(); // ThreadID is a ThreadLocal field
        int j= 1 - i;
        flag[i] = true;    // I'm Interested
        victim = i;        // You go first
        while(flag[j] && victim == i){}; //wait
   }
   public void unlock(){
       int i = ThreadID.get();
       flag[i] = false;      //Not interested anymore
   }
 }
person John Vint    schedule 26.05.2010
comment
Спасибо за предложение. Это как-то отличается от того, что я написал? - person Gabriel Ščerbák; 26.05.2010
comment
Я не вижу никакой реальной разницы, кроме реализации Lock. Ваш выглядит правильно, я публиковал это, чтобы показать как замок и указать на хорошую ссылку на книгу, которую я предложил. - person John Vint; 26.05.2010
comment
W Я понимаю, спасибо, я рад понять это правильно, хотя, как указано в комментариях к вопросу, компилятор нарушает важные предварительные условия, что делает этот алгоритм неправильным для семантики модели памяти Java. - person Gabriel Ščerbák; 26.05.2010

Хотя это и не алгоритм Патерсона, классы AtomicBoolean и Atomic* используют подход незаблокированных циклов занятости для обновления общих данных. Они могут соответствовать вашим требованиям.

person Peter Lawrey    schedule 26.05.2010