
ЛЕКЦИЯ 12
Классические проблемы межпроцессорного взаимодействия
Проблема обедающих философов.
Классическая проблема, сформулированная Дейкстрой в 1965 г.
5 философов проводят свою жизнь за размышлениями и едой. На столе стоит кастрюля со спагетти, у каждого философа есть тарелка. Между каждыми тарелками лежит одна вилка.
Рис.1
Собственно, проблема в том, что спагетти очень скользкие, и философу нужно две вилки, чтобы с ними управиться. Получив две вилки одновременно, философ ест, затем кладет вилки на стол (и размышляет).
Если хотя бы одна из вилок рядом с тарелкой занята, философ ждет.
Требуется написать алгоритм, моделирующий поведение философа так, чтобы при параллельном исполнении не возникало тупиков.
Начинать решение удобно с анализа таких ситуаций, когда возникает тупик. Например, если каждый философ одновременно возьмет левую (или правую) вилку, то никому не достанется правой.
Выход из положения: после захвата одной вилки проверить доступность правой. Если она недоступна, положить взятую вилку и подождать. Но это тоже неправильно: при одновременном захвате одной вилки все процессы будут вынуждены одновременно вернуть вилку на стол; подождав одинаковое количество времени, они опять одновременно повторят те же действия, и процессы зависнут.
Очевидно, что нельзя ждать одинаковое время; правильное решение – случайное ожидание. Тогда процессы зависнут только в том случае, если случайно выпадут одинаковые времена ожидания; вероятность этого низка. Такое решение применяется в сети Ethernet: если две станции посылают пакеты одновременно, возникает коллизия, и по протоколу каждая станция должна подождать случайное время, а потом повторить попытку. Это обычно хорошо работает. Однако, можно решить задачу без случайных чисел. Одно из решений неэффективное, но верное: защитить доступ к вилкам (и к еде вообще) мьютексом. Но тогда в каждый момент времени сможет есть только один философ. А из рис.1 видно, что одновременно могут есть 2.
Хорошее решение:
#define N 5 //количество философов
#define LEFT (i+N+1)%N //номер левого соседа
#define RIGHT (i=N-1)%N //номер правого соседа
#define THINKING 0 //думает
#define HUNGRY 1 //голоден
#define EATING 2 //ест
typedef int semaphore;
int state[N]; //массив для отслеживания состояний каждого философа
semaphore mutex=1;
semaphore S[N]; //каждому философу по семафору
//-------------------------------------------------------------------------------------------------
void philosopher (int i) //i – номер философа от 0 до N-1
{
while(1)
{
think();
take_forks(i);
eat();
put_forks(i);
}
}
void take_forks (int i)
{
wait(mutex); //вход в CS
state[i]=HUNGRY; //фиксация наличия голодного философа
test(i); //фиксация наличия голодного философа
release(mutex); //выход из CS
wait(S[i]); //блокировка, если вилок не досталось
}
void test (int i)
{
if(state[i]==HUNGRY && state[LEFT)!=EATING)
{
state[i]=EATING;
signal(S[i]);
}
}
Здесь философ как бы сам себя пропускает, открывая семафор. Если же условие ложно, семафор не откроется, и процедура take_forks будет блокирована.
void put_forks (int i)
{
wait(mutex); //вход в CS
state[i]=THINKING; //философ перестал есть
test(LEFT); // проверить, могут ли есть
test(RIGHT); //соседи слева и справа
release (mutex); //выход из CS
}
Проблема спящего брадобрея (или задача о парикмахерской).
В парикмахерской есть один брадобрей, его кресло и n стульев для посетителей. Если желающих побриться нет, брадобрей сидит в своем кресле и спит. Если в парикмахерскую приходит клиент, а брадобрей спит, клиент будит его. Если же брадобрей занят, то клиент садится на стул или уходит, если все стулья заняты. Необходимо запрограммировать брадобрея и посетителей.
Предлагаемое решение:
Используются три семафора:
Customers – для подсчета ожидающих посетителей (без того, который уже стрижется);
Barbers – количество простаивающих в ожидании брадобреев (0 или 1);
Mutex – для реализации взаимного исключения.
Также используется переменная waiting для подсчета ожидающих посетителей. Она является копией customers и нужна для того, чтобы заглядывающий клиент мог сосчитать количество занятых стульев и уйти, если мест уже нет. При отсутствии неблокирующего ожидания на семафоре копия customers – единственный выход.
#define CHAIRS 5 //количество стульев
typedef int semaphore;
semaphore customers=0;
semaphore barbers=0;
semaphore mutex=1;
int waiting=0;
void barber(void)
{
while(1)
{
wait(customers); //ждать посетителей
wait(mutex); //вход в CS
waiting--; //ждет уже на одного меньше
signal(barbers); // на одного больще брадобрея доступно
release (mutex); // выход из СS
cut_hair(); //обслуживание – вне CS
}
}
void customer(void)
{
wait(mutex); //вход в CS
if(waiting<CHAIRS)
{
//если есть места
waiting++; //ожидающих на 1 больше
signal(customers); //разбудить брадобрея, если надо
release(mutex); //выход из CS
wait(barbers); //ждать, пока брадобрей не освободится
get_haircut(); //постричься
}
else release(mutex); //много посетителей – уйти, освободить mutex
}
Приходя, клиент запрашивает mutex; если придут сразу два клиента или более, только один из них сможет выполнять процедуру customer.
Если свободный стул есть, посетитель увеличивает число ожидающих и активизирует брадобрея. Но начать стричься он не может, пока брадобрей занят. Брадобрей, приходя на работу, сначала блокируется на семафоре customers. Когда придет клиент и активизирует его, брадобрей захватит mutex и уменьшит число ожидающих, усадив одного клиента в кресло.
Замечание: не смотря на отсутствие передачи данных между процессами, рассмотренные задачи являются задачами межпроцессорного взаимодействия, т.к. они требуют синхронизации действий процессов.
Это в некоторой степени относится и к следующей задаче.
Проблема читателей и писателей
Эта задача моделирует доступ к БД. Разрешаются одновременно чтение, но требуется монопольный доступ на запись, и в это время не разрешается даже чтение.
semaphore mutex =1;
semaphore db =1; //контроль доступа к БД
int rc=0; //количество процессов чтения
void reader(void)
{
while(1)
{
wait(mutex); //вход в CS
rc++; //одним читающим больше
if(rc==1)
wait(db); //если этот читающий процесс первый – получить доступ к базе
//остальные – просто увеличивают на 1 число читающих (и тоже получают доступ)
release(mutex); //вход в CS
read_database(); //доступ к данным
wait(mutex); //вход в CS
rc--; //одним читающим меньше
if(rc==0) //если читающий был последним
signal(db); //отказаться от доступа к БД
release(mutex); //выход из CS
use_data(); //использовать данные
}
}
void writer(void)
{
while(1)
{
thinkup_data(); //подготовить данные
wait(db);
write_database(); //записать данные
signal(db); //лучше release(db) – это тоже вроде как mutex
}
}
В этом решении есть один недостаток. Читатели друг другу не мешают, и их может быть много. Но пишущий процесс не может писать, пока есть кто-то из читающих. Пока в базе есть хотя бы один читающий процесс, можно приходить любому количеству читателей. Так что, если активность читателей велика, пишущий процесс будет постоянно находиться в состоянии ожидания. Этого легко избежать, обеспечив полное разделение доступа – новый читающий процесс будет наравне с пишущим бороться за право доступа к БД. Однако, тогда задача как таковая пропадает – получается просто критическая секция.
Наилучшим решением является некоторое повышение приоритета пишущего процесса без изменения алгоритма.