stringtranslate.com

Алгоритм пекарни Лампорта

Пекарский алгоритм Лэмпорта — это компьютерный алгоритм , разработанный учёным-компьютерщиком Лесли Лэмпортом в рамках его длительного исследования формальной корректности параллельных систем , который предназначен для повышения безопасности использования общих ресурсов несколькими потоками посредством взаимного исключения .

В информатике часто бывает так, что несколько потоков одновременно обращаются к одним и тем же ресурсам. Повреждение данных может произойти, если два или более потоков попытаются записать данные в одну и ту же ячейку памяти или если один поток считывает данные из ячейки памяти до того, как другой закончит запись в нее. Алгоритм Lamport's bakery — один из многих алгоритмов взаимного исключения , разработанных для предотвращения одновременного входа параллельных потоков в критические разделы кода, чтобы исключить риск повреждения данных.

Алгоритм

Аналогия

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

Согласно аналогии, «клиенты» — это потоки, обозначенные буквой i , полученной из глобальной переменной.

Из-за ограничений компьютерной архитектуры некоторые части аналогии Лампорта требуют незначительной модификации. Возможно, что более чем один поток получит одно и то же число n , когда они его запросят; этого нельзя избежать (без предварительного решения проблемы взаимного исключения, что является целью алгоритма). Поэтому предполагается, что идентификатор потока i также является приоритетом. Более низкое значение i означает более высокий приоритет, и потоки с более высоким приоритетом войдут в критическую секцию первыми.

Критический раздел

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

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

В псевдокоде это сравнение между потоками a и b можно записать в виде:

// Пусть n a - номер клиента для потока a , и// i a - номер потока для потока a , тогдаа , я а ) < (н б , я б ) 

что эквивалентно:

(n a < n b ) или ((n a == n b ) и ( ia < i b ))

Как только поток завершает свою критическую работу, он избавляется от своего номера и переходит в некритическую секцию .

Некритический раздел

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

Эта часть аналогична действиям, которые происходят после совершения покупок, например, возвращение сдачи в кошелек.

Реализация алгоритма

Определения

В оригинальной статье Лэмпорта входящая переменная известна как выбор , и применяются следующие условия:

Примеры кода

Псевдокод

В этом примере все потоки выполняют одну и ту же "главную" функцию Thread . В реальных приложениях разные потоки часто имеют разные "главные" функции.

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

 // объявление и начальные значения глобальных переменных Ввод : массив [ 1. . NUM_THREADS ] из bool = { false };       Число : массив [ 1. . NUM_THREADS ] целого числа = { 0 };       блокировка ( целое число i ) {   Ввод [ i ] = true ;   Число [ i ] = 1 + макс ( Число [ 1 ], ..., Число [ NUM_THREADS ]);       Ввод [ i ] = false ;   для ( целое число j = 1 ; j <= NUM_THREADS ; j ++ ) {          // Подождите, пока поток j получит свой номер: пока ( Ввод [ j ]) { /* ничего */ }     // Подождать, пока все потоки с меньшими номерами или с одинаковыми // число, но с более высоким приоритетом, завершают свою работу: while (( Число [ j ] != 0 ) && (( Число [ j ], j ) < ( Число [ i ], i ))) { /* ничего */ }             } }  разблокировать ( целое число i ) {   Число [ i ] = 0 ;   } Поток ( целое число i ) {   в то время как ( правда ) {   замок ( я ); // Критическая секция располагается здесь... разблокировать ( i ); // некритическая секция... } }

Каждый поток записывает только свое собственное хранилище, только чтение является общим. Примечательно, что этот алгоритм не построен поверх какой-либо низкоуровневой «атомарной» операции, например, сравнения и обмена . Исходное доказательство показывает, что для перекрывающихся чтений и записей в одну и ту же ячейку хранилища только запись должна быть корректной. [ необходимо разъяснение ] Операция чтения может возвращать произвольное число. Следовательно, этот алгоритм можно использовать для реализации взаимного исключения в памяти, в которой отсутствуют примитивы синхронизации, например, простой диск SCSI, общий для двух компьютеров.

Необходимость переменной Entering может быть неочевидной, поскольку нет «блокировки» вокруг строк 7–13. Однако предположим, что переменная была удалена и два процесса вычислили одно и то же Number[i]. Если процесс с более высоким приоритетом был вытеснен до установки Number[i], процесс с низким приоритетом увидит, что другой процесс имеет номер ноль, и войдет в критическую секцию; позже процесс с высоким приоритетом проигнорирует equal Number[i]для процессов с более низким приоритетом и также войдет в критическую секцию. В результате два процесса могут войти в критическую секцию одновременно. Алгоритм пекарни использует переменную Entering , чтобы назначение в строке 6 выглядело как атомарное; процесс i никогда не увидит число, равное нулю, для процесса j , который собирается выбрать то же число, что и i .

При реализации псевдокода в системе с одним процессом или в кооперативной многозадачности лучше заменить разделы "ничего не делать" кодом, который уведомляет операционную систему о необходимости немедленного переключения на следующий поток. Этот примитив часто называют yield.

Алгоритм пекарни Лампорта предполагает последовательную модель памяти согласованности. Немногие, если таковые имеются, языки или многоядерные процессоры реализуют такую ​​модель памяти. Поэтому правильная реализация алгоритма обычно требует вставки ограждений для предотвращения переупорядочивания. [1]

PlusCalкод

Мы объявляем N числом процессов и предполагаем, что N — натуральное число.

КОНСТАНТА NПРЕДПОЛАГАЕМ N \in Nat

Мы определяем P как множество {1, 2, ... , N} процессов.

П == 1..Н

Переменные num и flag объявлены как глобальные.

--алгоритм AtomicBakery {переменная num = [i \in P |-> 0], флаг = [i \in P |-> FALSE];

Следующее определение LL(j, i)истинно тогда и только тогда, когда <<num[j], j>> меньше или равно <<num[i], i>> в обычном лексикографическом порядке .

определить { LL(j, i) == \/ num[j] < num[i] \/ /\ число[i] = число[j] /\ j =< я }

Для каждого элемента в P существует процесс с локальными переменными unread, max и nxt. Шаги между последовательными метками p1, ..., p7, cs считаются атомарными. Оператор с устанавливает id для недетерминированного выбранного элемента множества S, а затем выполняет тело. Шаг, содержащий оператор await expr, может быть выполнен только тогда, когда значение expr равно TRUE .(x \in S) { body }

процесс (p \in P) переменные непрочитанные \в SUBSET P, макс \в Нат, nxt \in P;{p1: пока (ИСТИНА) { непрочитано := P \ {self} ; макс := 0; флаг[self] := ИСТИНА;p2: пока (непрочитано # {}) { с (i \in непрочитано) { непрочитано := непрочитано \ {i}; если (число[i] > макс) { макс := число[i]; } } };p3: num[self] := max + 1;p4: флаг[self] := ЛОЖЬ; непрочитано := P \ {self} ;p5: пока (непрочитано # {}) { с (i \in непрочитано) { nxt := i ; }; ждем ~ флаг[nxt];p6: ожидание \/ num[nxt] = 0 \/ LL(self, nxt) ; непрочитано := непрочитано \ {nxt}; } ;cs: пропустить ; \* критический раздел;p7: num[self] := 0; }}}

Java-код

Мы используем класс AtomicIntegerArray не из-за его встроенных атомарных операций, а потому, что его методы get и set работают как изменчивые чтения и записи. В рамках модели памяти Java это гарантирует, что записи немедленно видны всем потокам.

AtomicIntegerArray ticket = new AtomicIntegerArray ( threads ); // тикет для потоков в очереди, n - количество потоков     // Java инициализирует каждый элемент 'ticket' значением 0 AtomicIntegerArray входящий = new AtomicIntegerArray ( threads ); // 1, когда поток входит в линию     // Java инициализирует каждый элемент 'entering' значением 0 public void lock ( int pid ) // идентификатор потока    { ввод . set ( pid , 1 );  целочисленный максимум = 0 ;    для ( int i = 0 ; i < потоки ; i ++ )         { int current = билет . получить ( i );    если ( текущий > макс .)    { макс = ток ;   } } билет . набор ( pid , 1 + max );     ввод . set ( pid , 0 );  для ( int i = 0 ; i < билет . длина (); ++ i )         { если ( я != pid )    { while ( entry . get ( i ) == 1 ) { Thread . yield (); } // ждем, пока другой поток выберет билет        пока ( билет.получить ( i ) ! = 0 && ( билет.получить ( i ) < билет.получить ( pid ) ||          ( билет . получить ( i ) == билет . получить ( pid ) && i < pid )))       { Thread . yield (); }   } } // Критическая секция располагается здесь...}публичная разблокировка void ( int pid )   { билет.set ( pid , 0 ) ; }

Смотрите также

Ссылки

  1. ^ Чинмей Нараян, Шибашис Гуха, С. Арун-Кумар Вывод ограждений в параллельной программе с использованием доказательства корректности SC

Внешние ссылки