
- •Лекція №8 Паралельні обчислення: семафори, монітори, повідомлення
- •8.1. Семафоры
- •8.1.1. Взаимные исключения
- •Листинг 8.2. Взаимоисключения с использованием семафоров
- •8.1.2. Задача “производителя-потребителя” ("писатель-читатель")
- •8.2. Мониторы
- •8.2.1. Мониторы с сигналами (мониторы Хоара)
- •8.2.2. Мониторы с оповещением и широковещанием
- •8.3.1. Синхронизация
- •8.3.2. Адресация
- •8.3.3. Формат сообщения
8.2.2. Мониторы с оповещением и широковещанием
Определение монитора, данное Хоаром, требует, чтобы в случае, если очередь ожидания выполнения условия не пуста, то при выполнении каким-либо процессом операции Csignal для этого условия, был немедленно запущен процесс, находящийся в указанной очереди. Таким образом, выполнивший операцию Csignal процесс должен либо немедленно выйти из монитора, либо быть приостановленным. У такого подхода есть два недостатка.
1. Если, выполнивший операцию Csignal, процесс не завершил свое пребывание, то требуется два дополнительных переключения процессов: одно для приостановки данного процесса и второе для возобновления его работы, когда монитор станет доступен.
2. Планировщик процессов, связанный с сигналом, должен быть идеально надежен. При выполнении Csignal процесс из соответствующей очереди должен быть немедленно активизирован, причем планировщик должен гарантировать, что до активизации никакой другой процесс не войдет в монитор. В противном случае условие, с которым активизируется процесс, может успеть измениться. Так, например, в листинге 9.5.1, когда выполняется выполнивший операцию Csignal(notempty), процесс из очереди notempty должен быть активизирован до того, как новый потребитель войдет в монитор. Если произойдет сбой процесса производителя непосредственно после того, как он добавит символ, так что операция Csignal не будет выполнена, то в результате процессы в очереди notempty окажутся навечно заблокированы.
Лэмпсон (Lampson) и Ределл (Redell) разработали другое определение монитора, который, в частности, применяется в языке программирования Modula-3. Их подход позволяет преодолевать описанные проблемы. Примитив Csignal заменен примитивом Cnotify, который интерпретируется следующим образом. Когда процесс, выполняющийся в мониторе, вызывает Cnotify(х), об этом оповещается очередь условия х, но выполнение вызвавшего Cnotify процесса продолжается. Результат оповещения состоит в том, что процесс в начале очереди условия возобновит свою работу в ближайшем будущем, когда монитор окажется свободным. Однако поскольку нет гарантии, что некий другой процесс не войдет в монитор до упомянутого ожидающего процесса, при возобновлении работы наш процесс должен еще раз проверить, выполнено ли условие. В случае использования такого подхода процедуры монитора ProducerConsumer будут иметь следующий вид (листинг 8.5).
Листинг 8.5. Код монитора ProducerConsumer
Void Append (char x) {
While (count == N)
cwait (notfull); /* Буфер заполнен */
Buffer [nextin] = x;
Nextin = (nextin+1) % N;
Count++; /* Добавляем элемент в буфер */
Cnotify (notempty); /* Уведомляем потребителя */
}
Void Take (char x) {
While (count == 0)
cwait (notempty); /* Буфер пуст */
x = Buffer [nextout];
Nextout = (nextout+1) % N;
Count--; /* Удаляем элемент из буфер */
Cnotify (notfull); /* Уведомляем производителя */
}
Инструкции if заменены циклами while. Таким образом, будет выполняться как минимум одно лишнее вычисление переменной условия. Однако в этом случае отсутствуют лишние переключения процессов, и не имеется ограничений на момент запуска ожидающего процесса после вызова Cnotify.
Одной из полезных особенностей такого рода мониторов может быть связанное с каждым примитивом условия Cnotify предельное время ожидания. Процесс, который прождал уведомления в течение предельного времени, помещается в список активных независимо от того, было уведомление о выполнении условия или нет. Такая возможность предотвращает бесконечное голодание процесса в случае сбоя других процессов перед уведомлением о выполнении условия.
При использовании правила, согласно которому происходит уведомление процесса, а не его насильственная активация, в систему команд можно включить примитив (например, cbroadcast - передача), который вызывает активацию всех ожидающих процессов. Это может быть в ситуациях, когда процесс не осведомлен о количестве ожидающих процессов. Предположим, например, что в программе “производитель-потребитель” функции Append и Тake могут работать с символьными блоками переменной длины. В этом случае, когда производитель добавляет в буфер блок символов, он не обязан знать, сколько символов готов потребить каждый из ожидающих процессов. Он просто выполняет инструкцию cbroadcast, и все ожидающие получат уведомление о том, что они могут попытаться получить свою долю символов из буфера (широковещательное сообщение).
Кроме того, широковещательное сообщение может использоваться в том случае, когда процесс не в состоянии точно определить, какой именно процесс из ожидающих должен быть активирован. Хорошим примером такой ситуации может служить диспетчер памяти. Допустим, у нас имеется j байт свободной памяти, и некоторый процесс освобождает дополнительно k байт. Диспетчеру не известно, какой именно из ожидающих процессов сможет работать с j+k байт свободной памяти; следовательно, он может использовать вызов cbroadcast, и все ожидающие процессы сами проверят, достаточно ли им свободной памяти.
Преимущество монитора Лэмпсона-Ределла по сравнению с монитором Хоара является его меньшая подверженность ошибкам. Поскольку каждая процедура после получения сигнала проверяет переменную монитора с использованием цикла While, то даже при передаче процессом неверного уведомления или широковещательного сообщения – это не приведет к ошибке в программе, получившей сигнал (попросту убедившись, что ее зря активизировали, программа вновь перейдет в состояние ожидания).
Приведем пример программы “производитель-потребитель” на языке программирования Java. Java – объектно-ориентированный язык программирования, поддерживающий потоки на уровне пользователя, и позволяющий группировать методы (процедуры) в классы. Добавление в описание метода ключевого слова synchronized гарантирует, что если хотя бы один поток начал выполнение этого метода, ни один другой поток не сможет выполнить другой синхронизированный (определенный как synchronized) метод из этого класса.
Листинг 8.6. Решение задачи “производитель-потребитель” на Java
public class ProducerConsumer {
static final int N = 100; // Размер буфера
static producer p = new producer(); //создать экземпляр потока производ.
static consumer c = new consumer(); // создать экземпляр потока потреб.
static our_monitor mon = new our_monitor(); //создать экземпляр монитора
public static void main(String args[]) {
p.start(); // запуск потока производителя
c.start(); // запуск потока потребителя
}
static class producer extends Thread {
public void run() { // метод run содержит программу потока
int item;
while (true) { // цикл производителя
item = produce_item();
mon.insert(item);
}
}
private int produce_item() {…} // собственно производство
}
static class consumer extends Thread {
public void run() { // метод run содержит программу потока
int item;
while (true) { // цикл потребителя
item = mon.remove();
consume_item(item);
}
}
private void consume_item(int item) {…} // собственно потреблен.
}
static class our_monitor { // монитор
private int buffer[] = new int[N];
private int count = 0, lo = 0, hi = 0; // счетчик и индексы
public synchronized void insert(int val) {
if (count == N) go_to_sleep(); //если буфер полн - ждать
buffer[hi] = val; // поместить элемент в буфер
hi = (hi+1) % N; // следующий сегмент для элемента
count = count+1; // счетчик элементов в буфере
if (count == 1) notify();// если потребитель в состоянии ожи-
} // дания, то активировать его
public synchronized int remove() {
int val;
if (count == 0) go_to_sleep(); //если буфер пуст - ждать
val = buffer[lo] ; // забрать элемент из буфера
lo = (lo+1) % N; // следующий сегмент для извлечения
count = count - 1; //теперь в буфере на 1 элемент меньше
if (count == N - 1) notify(); //если производитель в состоянии
// ожидания, то активировать его
return val;
}
private void go_to_sleep() {
try { wait(); }
catch(interruptedException exc);
}
}
}
В программе производителя есть бесконечный цикл формирования данных и помещения их в общий буфер. В коде потребителя есть бесконечный цикл с изъятием данных из общего буфера и их обработкой.
Интерес для нас представляет класс our_monitor, содержащий буфер, переменные администрирования и два метода синхронизации. Когда производитель активен в процедуре insert, потребитель не может быть активен в процедуре remove, что исключает состояние состязания. Переменная cont содержит количество элементов в буфере, принимая значения от 0 до N-1. Переменная lo является индексом следующего буфера, из которого следует извлечь данные. Переменная hi является индексом следующего буфера, в который следует поместить данные. Разрешена ситуация, когда lo = hi, что означает 0 или N элементов в буфере. Различать эти два случая можно по переменной count.
Синхронизированные методы в языке Java отличаются от стандартных мониторов отсутствием переменных состояния. Взамен предлагается две процедуры wait и notify, которые используются в синхронизированных методах, а это исключает состязания. Теоретически процедура может быть прервана, для чего и служит весь окружающий ее набор программ. В нашем случае просто представьте, что go_to_sleep описывает уход в состояние ожидания. Последний пример написан на Java, а не на С, как все остальные примеры, так как С и многие другие языки не имеют мониторов (кроме Modula-3).
8.3. Передача сообщений
Семафоры и мониторы вообще-то были разработаны для решения задачи взаимного исключения в системе с одним или несколькими процессорами, имеющими доступ к общей памяти. Эти примитивы будут неприменимы в распределенной системе, состоящей из нескольких процессоров с собственной памятью у каждого, так как все они базируются на использовании разделяемой оперативной памяти. Например, два процесса, которые взаимодействуют, используя семафор, должны иметь доступ к нему. Если оба процесса выполняются на одной и той же машине, они могут иметь совместный доступ к семафору, хранящемуся, например, в ядре, делая системные вызовы. Однако, если процессы выполняются на разных машинах, то этот метод не применим, для распределенных систем нужны новые подходы.
Вывод из всего вышесказанного следующий: семафоры являются примитивами слишком низкого уровня, а мониторы могут использоваться только в некоторых языках программирования. Эти же примитивы не подходят для реализации обмена информацией между компьютерами – нужно что-то другое.
При взаимодействии процессов между собой должны удовлетворяться два фундаментальных требования: синхронизации и коммуникации. Процессы должны быть синхронизированы, чтобы обеспечить выполнение взаимных исключений. Сотрудничающие процессы должны иметь возможность обмениваться информацией. Одним из подходов к обеспечению обеих указанных функций является передача сообщений. Важным достоинством передачи сообщений является ее пригодность для реализации как в одно- и многопроцессорных системах с разделяемой памятью, так и в распределенных системах.
Системы передачи сообщений могут быть различных типов, мы же рассмотрим наиболее общие возможности и свойства таких систем. Обычно функции передачи сообщений представлены в виде пары примитивов
send (<получатель>, <сообщение>) и receive(<отправитель>,<сообщение>),
которые скорее являются системными вызовами, чем структурными компонентами языка, что отличает их от мониторов и делает похожими на семафоры.
Процесс посылает информацию в виде <сообщение> другому процессу, определенному как <получатель>, вызовом send. Получает информацию процесс при помощи выполнения примитива receive, которому указывает отправителя сообщения.
С системами передачи сообщений связано большое количество сложных проблем и конструктивных вопросов, которые не возникают в случае семафоров и мониторов. Особенно много сложностей появляется в случае взаимодействия процессов, происходящих на различных компьютерах, соединенных сетью. Так, сообщение может потеряться в сети. При разработке систем передачи сообщений следует решить ряд вопросов, которые перечислены в табл. 8.1.
Синхронизация Отправление Блокирующее Неблокирующее Получение Блокирующее Неблокирующее Проверка наличия Адресация Прямая Отправление Получение Неявное Явное Косвенная Статическая Динамическая Владение |
Формат Содержимое Длина Фиксированная Переменная
Принципы работы очереди FIFO Приоритетная |
Таблица 8.1. Характеристики систем передачи сообщений