4G_L18

реклама
43
ЛЕКЦИЯ 18 (21/05/04)
ПРИЛОЖЕНИЕ
КЛАССИЧЕСКИЕ ЗАДАЧИ ТЕОРИИ ОПЕРАЦИОННЫХ СИСТЕМ
При решении различных задач, при разработке и синхронизации
параллельных вычислений бывает полезно сопоставить решаемую
задачу с известными схемами и проверить применимость полученного
решения к известной задаче. Существует ряд таких "эталонных"
задач, которые отражают наиболее часто возникающие перед
разработчиками проблемы.
§1 Схема "производитель-потребитель"
Эта схема подробно рассматривалась ранее, поэтому здесь
только вспомним о ее существовании в ряду классических задач
теории ОС.
§2 Схема "читатели-писатели"
Постановка задачи:
Пусть имеются некоторые данные, совместно используемые рядом
процессов. Эти данные могут находиться в файле на диске, в блоке
основной памяти или в регистрах процессора. Имеется несколько
процессов, которые только читают эти данные ("читатели") и
несколько других, которые только записывают данные ("писатели").
При этом должны удовлетворяться следующие условия:
1) любое число "читателей" может одновременно читать данные
(пусть это будет для определенности, например, файл);
2) записывать информацию в файл в определенный момент
времени может только один "писатель";
3) когда "писатель" записывает данные в файл, ни один
"читатель" не может его читать.
В описанной схеме "читатели" не изменяют данные, а "писатели"
не считывают информацию. Задача в такой постановке является
сужением более общей задачи, в которой каждый процесс может
являться как читателем, так и писателем. В таком случае можно
любую часть процесса, которая обращается к данным, объявить
критическим разделом, и использовать простейшее решение на
основе взаимоисключений. Схема "читатели-писатели" является
достаточно распространенным частным случаем этой общей задачи,
для которого имеется гораздо более эффективное решение. Именно
поэтому ее принято выделять в отдельную задачу.
Примеры
1. Представим себе библиотечный каталог, в котором читатели
могут искать нужную им литературу, а работники библиотеки могут
этот каталог обновлять. Работники при внесении изменений не
должны мешать друг другу, а также не должны допускать читателей
к данным в момент их изменения, чтобы предотвратить получение
читателями недостоверной информации. Читатели могут пользоваться
каталогами по несколько человек одновременно. Если рассматривать
44
общее решение, то читатели были бы вынуждены входить в каталог
по одному, что вызвало бы неоправданные задержки и очереди.
2. Классическим примером рассматриваемой схемы является система
предварительной продажи билетов (авиа или железнодорожных).
Запросы кассира-оператора играют роль "читателя", и таких
запросов к БД (базе данных) может поступить гораздо больше, чем
реальных изменений БД. Когда клиент (возможно, после нескольких
запросов) выберет себе рейс и купит билет, тогда вступит в
действие процесс-писатель – кассир внесет в базу изменения о
наличии свободных мест на соответствующий рейс. При этом кассир
выступает сначала в роли "читателя", а затем в роли "писателя",
точнее, запускает то один, то другой процесс.
Можно ли рассматривать схему "производитель-потребитель" в
качестве частного случая задачи "читатели-писатели" с одним
"читателем" (потребитель) и одним "писателем" (производитель)?
Оказывается, нет. Производитель – не просто писатель. Он должен
следить за указателем очереди, чтобы определить, куда надо
вносить очередную порцию информации, и контролировать состояние
буфера,
чтобы
исключить
возможность
его
переполнения.
Аналогично, потребитель – не просто читатель; он изменяет
значение указателя очереди, показывая тем самым, что информация
считана, и удаляет ее из буфера. Кроме того, он должен
контролировать наличие информации в буфере.
Рассмотрим два варианта решения поставленной задачи – с
приоритетным
чтением
и
с
приоритетной
записью.
Решения
справедливы
для
любого
количества
как
читателей,
так
и
писателей.
2.1 Приоритетное чтение
Приоритетное чтение означает, что процесс-писатель будет
допущен к разделяемым данным только тогда, когда все процессычитатели закончат свою работу.
Нам потребуется семафор для запрета доступа писателям в
критическую секцию. Пусть это будет семафор W. Поскольку
необходимо
организовать
одновременную
работу
нескольких
читателей, то состояние этого семафора должен проверять (и
изменять)
только
первый
читатель,
получивший
доступ
к
разделяемым данным. Аналогично, если последний находившийся в КС
читатель заканчивает чтение, только он должен открывать семафор
W. Следовательно, необходимо иметь счетчик количества читателей,
находящихся одновременно в КС – пусть это будет R_Count. Для
корректного изменения значения этого счетчика читателей нужно
ввести еще один дополнительный семафор S_R, который будет
закрываться только на время изменения счетчика R_Count.
Итак, рассмотрим программное решение:
45
Program R&W_prior_Reader;
Var R_Count: Integer;
W, S_R : Semaphore;
Procedure Writer;
Begin
While true Do
Begin
P(W);
{проверка (закрытие) своего семафора}
ЗАПИСЬ;
V(W);
{освобождение своего семафора}
End;
End;
Procedure Reader;
Begin
While true Do
Begin
P(S_R);
{если закрыт W, здесь читатели будут ждать}
Inc(R_Count); {читателей стало больше}
If R_Count=1 Then P(W);
V(S_R);
ЧТЕНИЕ;
P(S_R);
Dec(R_Count);
If R_Count=0 Then V(W);
V(S_R);
End;
End;
Begin
InitSem(W,1); InitSem(S_R,1);
R_Count:=0;
Parbegin
Reader; Writer;
Parend;
End.
Недостатком рассмотренного варианта является тот, что при
большом наплыве читателей писатель может оказаться в состоянии
бесконечного ожидания. Схема с приоритетом писателей позволяет
избежать этого явления.
2.2 Приоритетная запись
Приоритетная запись означает, что при появлении первого же
писателя прекращается доступ новых читателей к разделяемым
данным (следовательно, нужно подсчитывать количество писателей
аналогично тому, как это было сделано с читателями.). Вновь
пришедшие читатели выстраиваются в очередь на своем семафоре –
значит, для них потребуется еще один семафор, R. Читатели,
находившиеся в КС, постепенно заканчивают работу, а доступ новых
закрыт. Следовательно, в какой-то момент последний выходящий из
КС читатель откроет семафор W и даст возможность писателю войти
в КС. Если за это время появились еще писатели, они ожидают на
семафоре W, который будет открыт прежде, чем семафор R,
46
следовательно, писатели получат преимущественное право работы с
разделяемыми данными.
Program R&W_prior_Writer;
Var R_Count, W_Count: Integer;
W, S_R, R, S_W : Semaphore;
Procedure Writer;
Begin
While true Do
Begin
P(S_W); Inc(W_Count);
If W_Count=1 Then P(R);
V(S_W);
P(W);
{проверка (закрытие) своего семафора}
ЗАПИСЬ;
V(W);
{освобождение своего семафора}
P(S_W); Dec(W_Count);
If W_Count=0 Then V(R);
V(S_W);
End;
End;
Procedure Reader;
Begin
While true Do
Begin
P(R);
{если закрыт R, здесь читатели будут ждать}
P(S_R);
Inc(R_Count); {читателей стало больше}
If R_Count=1 Then P(W);
V(S_R);
V(R);
{нужно впустить остальных читателей}
ЧТЕНИЕ;
P(S_R);
Dec(R_Count);
If R_Count=0 Then V(W);
V(S_R);
End;
End;
Begin
InitSem(W,1); InitSem(S_R,1);
InitSem(R,1); InitSem(S_W,1);
R_Count:=0; W_Count:=0;
Parbegin
Reader; Writer;
Parend;
End.
В данном варианте проблемы могут возникнуть при большом
количестве писателей – читатели будут ожидать бесконечно. Чтобы
этого не случилось, можно использовать различные способы
улучшения алгоритма
–
например, проверять время ожидания
читателя в очереди или длину этой очереди (в случае, если
накопится очередь определенной длины, пропустить читателей и
снова восстановить прежний режим.)
47
2.3 Реализация схемы при помощи мониторов
При реализации с помощью монитора потребуется два условия –
назовем
их
Reading_allow,
Writing_allow,
а
также
общие
переменные
R_Count
(счетчик
числа
читателей)
и
Writing
(логическая переменная, истинное значение которой означает, что
идет запись). Для начала или окончания чтения или записи процесс
должен обратиться с вызовом соответствующей процедуры монитора.
Команды WAIT и SIGNAL рассматривались при изучении мониторов.
monitor Readers_Writers;
Var R_Count : Integer;
Writing : Boolean;
Reading_allow, Writing_allow : condition;
Procedure Start_Reading;
Begin
If Writing or очередь (Writing_allow) Then
WAIT(Reading_allow);
inc(R_Count);
SIGNAL(Reading_allow);
End;
Procedure Finish_Reading;
Begin
dec(R_Count);
If R_Count=0 Then
SIGNAL(Writing_allow);
End;
Procedure Start_Writing;
Begin
If Writing or (R_Count>0) Then
WAIT(Writing_allow);
Writing:=true;
End;
Procedure Finish_Writing;
Begin
Writing:=false;
If очередь (Reading_allow) Then
SIGNAL(Reading_allow)
Else SIGNAL(Writing_allow)
End;
Begin {main}
R_Count:=0;
Writing:=false;
End.
§3 задача "об обедающих философах"
Дейкстра
сформулировал
эту
любопытную
задачу,
которая
иллюстрирует многие ситуации, характерные для параллельного
программирования, в частности, проблемы взаимоблокировки и
голодания. Она может рассматриваться как типичная задача,
возникающая в многопоточных приложениях при работе с совместно
48
используемыми ресурсами, и, следовательно, может выступать в
роли тестовой при разработке новых подходов к синхронизации.
Постановка задачи:
Пятеро
философов
сидят
за
круглым
столом. Жизнь каждого из них предельно
проста:
некоторое
время
он
предается
размышлениям,
а
некоторое
время
ест
спагетти. Перед каждым философом находится
блюдо спагетти, которое постоянно пополняет
специальный слуга. На столе лежит ровно 5
вилок,
по
одной
между
любыми
двумя
философами-соседями. Чтобы есть спагетти,
философ в соответствии с правилами хорошего
тона должен пользоваться двумя вилками –
правой
и
левой.
(На
рисунке
вилки
обозначены стрелочками)
Задача состоит в том, чтобы разработать алгоритм, который
обеспечит взаимоисключение, т.к. двое не могут пользоваться
одной вилкой одновременно, а также не допустит ситуаций
взаимоблокировки (тупик) и голодания (бесконечное откладывание).
Пусть вилки и философы пронумерованы от 1 до 5, для
определенности по часовой стрелке. Тогда можно любую вилку
идентифицировать по ее номеру или по месту относительно
конкретного философа (левая или правая вилка). Для формального
описания
алгоритма
предпочтительнее
первый
вариант,
для
словесного – второй.
Тогда процедура Philosopher(i) будет описывать поведение i-го
философа и в общем случае выглядеть следующим образом:
Procedure Philosopher(i);
Begin
While true Do
Begin
Thinking; {мыслит некоторое время}
Take(i); {берет i-ю вилку – пусть это будет его правая
вилка}
Take(i mod 5 + 1); {это левая вилка}
Eating; {ест некоторое время}
Put(i); {кладет i-ю вилку}
Put(i+1); {кладет вторую вилку}
End;
End;
Каким же должен быть алгоритм действий, который позволит
исключить неприятные ситуации (см. выше)? Возможны следующие
варианты развития событий:
1)
Каждый философ берет – именно так, как описано в процедуре –
сначала правую, затем левую вилку. Возможная ситуация: все
философы одновременно возьмут свои правые вилки – возникнет
полная блокировка всей системы (требуемый для окончания
работы ресурс занят, а поток, который его держит, ждет
освобождения ресурса другим потоком, а тот, в свою очередь,
также ждет…).
2)
49
Каждый философ берет обе вилки одновременно (если они
свободны). Возможная ситуация: у некоторого конкретного
философа будут поочередно есть то его левый, то правый сосед,
вследствие чего он может умереть с голоду – возникнет
голодание (процесс длительное время не может получить
требуемого ему ресурса, хотя вся система в целом работает).
3)
Каждый философ берет сначала правую, затем левую вилку. Если
левой вилки нет, то он кладет обратно уже взятую им правую
вилку (временный отказ от запроса для снятия возможной
блокировки, сброс флага), а затем возобновляет попытки по той
же схеме. Возможная ситуация: при определенном стечении
обстоятельств получаем первый вариант, если философы начнут
действовать "синхронно".
4)
Введем в компанию философов некоторое разнообразие. Пусть они
имеют некоторые различия, а именно, есть "правые" философы и
"левые" – название философа соответствует тому, какую вилку
он берет в первую очередь. В таком случае, если во всей
компании будет присутствовать хотя бы один философ другого
типа, чем все остальные (хотя бы один "левый" и хотя бы один
"правый", остальные – все равно какие), система будет
функционировать без блокировок и голодания. Покажите, что это
действительно так.
Скачать