- •Лекция 6: Реализация взаимного исключения
- •Способы реализации взаимного исключения
- •На основе инструкций чтения-записи памяти
- •На основе запрета / разрешения прерываний (однопроцессорные)
- •На основе инструкции «Test & Set»
- •Семафоры
- •Class Semaphore {
- •Реализация семафоров для многопроцессорных машин p: V:
- •Счетчик событий
На основе инструкции «Test & Set»
Запрет прерываний не работает на многопроцессорных машинах!
Запрет прерываний на многопроцессорной машине запрещает переключение контекста на одном процессоре, но не препятствует нити на другом процессоре войти в критическую секцию. Запрет прерываний сразу на всех процессорах, очевидно, не самое удачное решение.
Все современные процессоры имеют в наборе такую атомарную инструкцию, которая в одно действие считывает, модифицирует и записывает значение переменной. В многопроцессорных архитектурах при обращении к общей переменной одновременно на двух процессорах гарантируется последовательное выполнение таких инструкций.
Пример: Инструкцияxchg (Intel x86) меняет местами содержимое регистра и ячейки памяти. Вспомним, что значение регистра всегда является локальным для нити, а переменная в памяти может быть общей (глобальной) для нескольких нитей. После атомарной операции обмена значение из памяти оказывается в локальном контексте и может быть спокойно проанализировано
Будем называть такие инструкции «test&set» и считать, что в одно действие эта инструкция считывает содержимое ячейки памяти и записывает в нее 1.
Как это работает?
Пусть переменная value = 0.
void Lock::Acquire()
{
while(test&set(value) == 1); //Ждем
}
void Lock::Release() { value) = 0; } //Освобождаем
Недостаток: - занятое ожидание; в результате которого задерживается выполнение нити, захватившей Lock.
Можно ли устранить занятое ожидание? – Нет!
Но зато можно минимизировать время занятого ожидания, если использовать его только для проверки и изменения обычной переменной, защищающей вход в критическую секцию. Фактически, необходимо реализовать двухуровневую защиту.
Применять можно, если минимизировать занятое ожидание, например, так:
Class Lock {
bool busy
int guard;
public:
Lock() { busy = false; guard = 0; }
void Acquire();
void Release();
};
void Lock::Acquire ()
{
while(test&set(quard));
if(busy) {
// поставить в очередь к этому Lock
schedule(&gaurd); // передать управление другому
// и сбросить guard
}
else {
busy = true;
guard = 0;
}
}
void Lock::Release ()
{
while(test&set(quard));
if(очередь не пуста) {
// исключить первого из очереди
// поставить в очередь готовых
}
else
busy = false;
guard = 0;
}
Резюме
Показано, что на базе простых аппаратных средств можно реализовать примитивы более высокого уровня для организации взаимного исключения.
Одна из первых реализаций взаимного исключения без занятого ожидания реализована в UNIX в виде системных вызовов(sleep и wakeup).
Особенности:
wakeup доsleep эквивалентен «пустому» действию и факт его выполнения не регистрируется
Если ждут несколько, то wakeup “разбудит ”всех сразу.
Написание параллельных программ – задача не простая, поэтому нужны «правильные» примитивы облегчающие синхронизацию.
Семафоры
Семафоры – примитивы для синхронизации параллельных процессов, предложенные (Edsger Dijkstra) в середине 60-х. Являлись основным примитивом синхронизации в раннемUNIXе. Сейчас используются вOS/2, Windows NT, UNIX.
С семафором связана целочисленная переменная, очередь ожидающих на семафоре процессов и две функции:
P (proberen)- атомарная операция, которая останавливает работу процесса(нити) до тех пор, пока переменная семафора станет положительной, затем уменьшает переменную на 1. Является аналогом операции wait.
V (verhogen)- атомарная операция, которая увеличивает переменную семафора на 1 и будит один процесс из очереди к семафору, если кто-то ждет. Является аналогом операцииsignal.