
Блокировки
При разработке ПО для распараллеливания выполнения задач используют потоки. При некоторых условиях в работы потоков может возникать взаимная блокировка, что приводит прекращению выполнения параллельных задач.
Поиск таких проблем обычно представляется сложной задачей. Для предотвращения таких ситуаций разработчики используют специальные алгоритмы и методы.
Требования, накладываемые на алгоритмы взаимного исключения.
- В любой момент времени в одном критическом разделе может находиться не более одного процесса.
- Процесс, завершающий работу в некритическом разделе, не должен влиять на работу остальных.
- Не должны возникать взаимные блокировки и голодания.
- Когда в критическом разделе нет ни одного процесса, любой процесс, запросивший доступ к нему, должен немедленно его получить.
- Алгоритмы должны работать для любого количества процессов и их относительной скорости работы.
- Любой процесс должен оставаться в критическом разделе только в течение ограниченного времени.
Пример: два процесса как потоки, т.е. могут польз. глоб. переменной.
Алгоритм 1. nTurn – очередь какого раздела наступила Код процесса 0 …while (nTurn != 0) /* пережидание */; // критический раздел nTurn = 1; // . . . Код процесса 1 while (nTurn != 1) /* пережидание */; // критический раздел nTurn = 0; // . . . Проблемы - строгое чередование; - блокировка при сбое другого процесса.
|
Алгоритм 2 Глоб. перем. – массив из 2х флагов. Глобальные переменные bool abFlags[2] = { false, false }; Код процесса 0 while (abFlags[1]) // (1) /* пережидание */ ; abFlags[0] = true; // (2) // критический раздел abFlags[0] = false; Код процесса 1 while (abFlags[0]) // (1) /* пережидание */ ; abFlags[1] = true; // (2) // критический раздел abFlags[1] = false; П В такой ситуации оба войдут в критический раздел
|
Алгоритм 3 Сначала отмечаем, что хотим войти, потом ожидаем. Код процесса 0 … abFlags[0] = true; // (1) … while (abFlags[1]) // (2) /* пережидание */ ; // критический раздел (3) abFlags[0] = false;… Код процесса 1 abFlags[1] = true; // (1) // while (abFlags[0]) // (2) /* пережидание */ ; // критический раздел (3) abFlags[1] = false; Проблемы: - могут быть обы навечно заблокированы (взаимная блокировка, true одновременно)
|
Алгоритм 4 Код процесса 0 abFlags[0] = true; // (1) while (abFlags[1]) // (2) { abFlags[0] = false; // (3) // задержка abFlags[0] = true; // (4) } // критический раздел abFlags[0] = false; Код процесса 1 abFlags[1] = true; // (1) while (abFlags[0]) // (2) { abFlags[1] = false; // (3) // задержка abFlags[1] = true; // (4) } // критический раздел abFlags[1] = false; Проблемы: Уступают друг другу > Бесконечно уступают друг другу (ленивая блокировка)
|
Алгоритм Деккера (Dijkstra, 1965)
Если два процесса пытаются перейти в критическую секцию одновременно, алгоритм позволит это только одному из них, основываясь на том, чья в этот момент очередь. Если один процесс уже вошёл в критическую секцию, другой будет ждать, пока первый покинет её. Это реализуется при помощи использования двух флагов (индикаторов "намерения" войти в критическую секцию) и переменной nTturn (показывающей, очередь какого из процессов наступила).
Код процесса 0 abFlags[0] = true; // (1) while (abFlags[1]) // (2) if (nTurn == 1) // (3) { abFlags[0] = false; // (4) while (nTurn == 1) // (5) /* пережидание */ ; abFlags[0] = true; // (6) } // критический раздел nTurn = 1; // (7) abFlags[0] = false; // (8) |
Код процесса 1 abFlags[1] = true; // (1) while (abFlags[0]) // (2) if (nTurn == 0) // (3) { abFlags[1] = false; // (4) while (nTurn == 0) // (5) /* пережидание */ ; abFlags[1] = true; // (6) } // критический раздел nTurn = 0; // (7) abFlags[1] = false; // (8)
|
Комбинация 1 и 4.
(2) – не освободили ли;
(3) – наступила ли очередь;
(4) – освобождаем
(5) – пережидаем чужую очередь
(6) – опять хотим войти
Блокировка с двойной проверкой. Не работает в SMP системах (архитектура многопроцессорных компьютеров, в которой два или более одинаковых процессоров подключаются к общей памяти), т.к. выполняется не в том порядке. Для решения проблемы ставится «барьер» (барьер памяти) - операция сброса кэша.
Другая проблема: nTurn не меняется в цикле, и компилятор выносит вне цикла. Решение – сделать переменную volatile. volatile - это квалификатор переменной, говорящий компилятору, что значение переменной может быть изменено в любой момент и что часть кода, которая производит над этой переменной какие-то действия (чтение или запись), не должна быть оптимизирована.
Алгоритм Петерсона (Peterson, 1981)
Программный алгоритм взаимного исключения потоков исполнения кода, разработанный Г. Петерсоном в 1981 г. Хотя изначально был сформулирован для 2-х поточного случая, алгоритм может быть обобщён для произвольного количества потоков. Алгоритм условно называется программным, так как не основан на использовании специальных команд процессора для запрета прерываний, блокировки шины памяти и т. д., используются только общие переменные памяти и цикл для ожидания входа в критическую секцию исполняемого кода.
Код процесса 0 abFlags[0] = true; // (1) nTurn = 1; // (2) while (abFlags[1] && // (3) nTurn == 1) // (4) /* пережидание */ ; // критический раздел abFlags[0] = false; // (5) |
Код процесса 1 abFlags[1] = true; // (1) nTurn = 0; // (2) while (abFlags[0] && // (3) nTurn == 0) // (4) /* пережидание */ ; // критический раздел abFlags[1] = false; // (5)
|
Когда нарушается одно из условий, входим в критический раздел. (Также volatile).
В С++ 2011 появились атомарные операции, которые запрещают менять местами (тогда уже нельзя назвать чисто программным алгоритмом)