Показаны сообщения с ярлыком Многопоточность. Показать все сообщения
Показаны сообщения с ярлыком Многопоточность. Показать все сообщения

пятница, 14 января 2011 г.

Многопоточное программирование. 3. Проблема производителя и потребителя

Проблема производителя и потребителя, известная так же как проблема ограниченного буфера, является одной из классических задач многопроцессной синхронизации.

Рассмотрим упрощенный случай с двумя процессами. Первый процесс - Производитель (Producer), второй - Потребитель (Consumer).
Процессы используют общий буфер: Производитель помещает информацию в буфер, в то время как Потребитель извлекает ее, независимо от Производителя. Для Производителя всегда должно присутствовать незанятое место (слот) для записи информации, тогда как для Потребителя информация для извлечения должна присутствовать всегда, т.е. как минимум один непустой слот.
Производитель надеется на Потребителя, что тот предоставит свободное место для вставки очередной порции информации, а Потребитель надеется на Производителя, что тот обеспечит информацию для извлечения.
В таком случае Производитель и Потребитель каким-то образом должны взаимодействовать между собой, чтобы знать, что очередная вставка/извлечение будет безопасной для работающего в данный момент одного из процессов.

На первый взгляд решение, представленное ниже, абсолютно верное:

#define MAX_SIZE 100
int count = 0;
Producer()
{
  while (TRUE)
  {
    item = ProduceItem();
    if (count == MAX_SIZE) // (1) если буфер полон
    sleep(); // (2)
    InsertItem(item);
    ++count;
    if (count == 1)// был ли буфер пуст
      wakeup(Consumer);
  }
}

Consumer()
{
  while(TRUE)
  {
    if (count == 0) // (3) если буфер пуст
      sleep(); // (4)
    item = RemoveItem();
    --count;
    if (count == MAX SIZE - 1) // был ли буфер полон
      wakeup(Producer);
    ConsumeItem(item);
  }
}
count - количество слотов информации (или товаров, кому как) в буфере.
MAX_SIZE - максимальный размер буфера
Производитель сначала проверяет, не полон ли буфер, и если буфер полон, то Производитель засыпает, если не полон, то помещает информацию, увеличивает счетчик занятых слотов, и проверяет, не был ли буфер пусто до этого, если был пуст - значит Потребитель спит, и его надо разбудить.
Потребитель же наоборот, засыпает, когда буфер оказывается пустым. Если же после извлечения очередного элемента, Потребитель узнает, что до этого буфер был полным, значит Производитель спит и его надо разбудить.
Но в жизни все не так :-)

Представим, что буфер полон и работает Производитель. Осуществляется проверка на полноту буфера, и внезапно после выполнения строки (1), т.е. когда стало уже известно что надо заснуть, произошло переключение процесса. Теперь работает Потребитель. Он исправно извлекает информацию. Очистив первый слот, Потребитель понимает что буфер был полон, и нужно разбудет спящего, как ему кажется, производителя. Потребитель посылает сигнал пробуждения, и очищает буфер до конца, после чего засыпает. После этого происходит переключение на процесс Производителя, который не спал, а только собирался, и теперь он готов осуществить свою мечту - немного поспать. И засыпает. Все спят. Взаимоблокировка или deadlock.
А проблема, как уже многие догадались, состоит в утраченном вызове активизации процесса. Т.е. Производитель не спал, а его будили. К аналогичному результату привело бы рассмотрение ситуации, если бы буфер оказался пустым, и Потребитель решил бы уснуть, но "не успел".

Итак. Попробуем устранить проблему утраченного сигнала. Для этого будем использовать семафоры.
Cемафор - переменная, как правило целочисленная, используется для синхронизации доступа множества параллельно работающих процессов/потоков к общему ресурсу. Между семафором и обыкновенной численной переменной есть различия:
  • Будучи созданным, семафору можно присвоить любое значение (для counting semaphore), но операции, которые могут работать с семафором, ограничиваются лишь инкрементом и декрементом
  • Когда процесс/поток уменьшает значение семафора, и результат становится отрицательным, то он блокирует сам себя, и не может продолжать выполнение, пока другой процесс/поток не увеличит значение семафора
  • Если процесс/поток инкрементирует значение семафора, и есть ожидающие процессы, то один из них разблокируется
В нашем решении мы будем использовать 2 вида семафоров:
  • Counting semaphore (подсчитывающий)
  • Binary semaphore (двоичный, или мьютекс)

Вообще говоря, мьютекc и двоичный семафор это не одно и то же. Правильнее будет рассуждать, что двоичный семафор может быть использован как мьютекс, но не наоборот.
Двоичный семафор будет использоваться для управления доступом к буферу (чтобы в данный момент только один из потоков, а их может быть очень много, имел доступ к критической секции)
Подсчитывающие семафоры буду использоваться для подсчета свободных и занятых слотов буфера
#define MAX_SLOTS 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N; // подсчет пустых слотов в буфере
semaphore full = 0; // подсчет заполненных слотов буфере
Producer()
{
  int item;
  while (TRUE)
  {
    item = ProduceItem();
    down(empty); // (1)
    down(mutex); // (2)
    EnterItem(item);
    up(mutex); // (3)
    up(full); // (4)
  }
}
Consumer()
{
  int item;
  while (TRUE)
  {
    down(full); // (5)
    down(mutex); // (6)
    item = RemoveItem();
    up(mutex); // (7)
    up(empty); // (8)
    ConsumeItem(item);
  }
}
Итак, mutex отвечает за доступ одного процесса/потока в данный момент времени. Разберемся с empty и full. Пусть какое-то время программа работает, затем буфер заполняется полностью. Пусть в это время работает Производитель. Если буфер заполнен полностью, то empty = 0, это значит, что команда down(empty) приведет к блокировке Производителя (вспоминаем пункт 2 о семафорах). Далее работает Потребитель, разгружает слоты, регулирует семафоры количества пустых и полных слотов (количество пустых растет), и после первого очищения слота, Производитель может быть разблокирован, т.к. значение семафора увеличилось. Допустим произошло переключени на Производителя. Он продолжает выполнение со строки (2), заходит в критическую секцию, заполняет слот, выходит и увеличивает количество заполненных ячеек, тем самым позволяя Потребителю (если тот заблокирован) разблокироваться. В том и разница, что в данном случае Производитель не производит насильную разблокировку Потребителю, не задумываясь, надо ли это ему, а лишь создает условия, необходимые для разблокировки в случае, если таковая потребуется. Читателю предлагается проверить оставшиеся три граничные ситуации:
  • Буфер полон, работает Потребитель
  • Буфер пуст, работает Производитель
  • Буфер пуст, работает Потребитель
Уфффф!

четверг, 21 октября 2010 г.

Многопоточное программирование. 2. Алгоритм Петерсона... чуть более подробно. ч.2. Случай N процессов

В прошлый раз был рассмотрен алгоритм Петерсона для взаимного исключения двух процессов. На этот раз рассмотрим алгоритм Петерсона, обощенный на N параллельно выполняющихся процессов.
Код алгоритма:
#define FALSE  0
#define N     10

int turn[N];
int stage[N + 1];

void enterRegion(int process)
{ 
  for (int i = 1; i <= N - 1; i++) // перебор стадий
  {
    stage[process] = i;
    turn[i] = process;
    for (int j = 1; j <= N; j++) // "опрос" остальных процессов
    {
      if (j == process)
        continue;
      while (stage[j] >= i  && turn[i] == process);
    }
  }
}

void leaveRegion(int process)
{
  stage[process] = FALSE;
}
Для простоты и улучшения читабельности кода не будем рассматривать процесс с индексом 0, так же не будем рассматривать стадию с индексом 0, точнее стадия с индексом 0 будет своеобразным "стартом".
А теперь немного теории.
Алгоритм Петерсона для N процессов по сути тот же самый, что и для случая двух процессов, однако перед входом в критическую процесс должен преодолеть (n-1) стадий, т.н. "состязания". Количество стадий равно (n-1), потому что на каждой стадии будет оставаться хотя бы один процесс, а остальные будут переходить на следующую стадию, т.е. не более (n-j) процессов могут покинуть стадию под номером j. В конце концов у нас останется только два процесса, один из которых пройдет в критическую секцию. Два процесса будут как раз на (n-1)-й стадии. Преодоление каждой стадии напоминает версию для двух процессов, где один из процессов временно устраняется.
stage[process] - номер стадии, в которой находится процесс с идентификатором process
turn[i] = process - идентификатор процесса process, зашедшего последним в стадию под номером i

Рассмотрим поведение процессов на рисунке.

Пусть поочередно все 4 процесса попали на первую стадию. Пусть последним оказался процесс 1.
Происходит "опрос" остальных процессов на наличие их на той же самой стадии, где находится работающий процесс, либо на стадиях с бОльшим номером. Пусть вследствие прерываний до цикла, где происходит опрос, добрался процесс 2. В этом случае условие
while (stage[j] >= i  && turn[i] == process)
не выполнится.
Несмотря на то, что на данной стадии кроме процесса 2 имеются еще процессы, процесс 2 НЕ является последним перешедшим на данную стадию (мы условились, что последним будет процесс 1). Стало быть процесс 2 проходит на следующую стадию. И если так случится, что прерываний не будет, и его никто не "догонит", то процесс 2 благополучно войдет в критическую секцию, затем выйдет из нее, присвоит переменной interested[process] значение FALSE, что будет означать что данный процесс находится на стадии 0, т.е. на "старте", ну или на самой "низшей" стадии.
Если же переключению процессов случаются примерно через равные промежутки времени, то всегда для последнего процесса, зашедшего на данную стадию, условие
while (stage[j] >= i  && turn[i] == process)
не будет выполняться по той простой причине, что он последний: (turn[i] == process).
Таким образом на последующую стадию переходит на один процесс меньше, и так пока не останется двух процессов, после чего один из двух войдет в критическую секцию. Второй из двух войдет в критическую секцию после выхода первого, потому что (stage[j] >= i) будет равно FALSE. А если второго процесса кто либо "догонит", тем не менее (turn[i] == process) будет равно FALSE, и процесс все равно войдет в критическую секцию.

суббота, 2 октября 2010 г.

Многопоточное программирование. 1. Алгоритм Петерсона... чуть более подробно


Разберем сегодня алгоритм Петерсона. Посвящается всем начинающим, продолжающим,
прогулявшим пары по операционным системам и тем, до кого не дошло сразу. Алгоритм
Петерсона позволяет добиться взаимного исключения двух процессов. Рассмотрим 3 варианта поведения двух процессов при возникновении прерываний в трех различных точках. Также в каждом из рассмотренных случаев будем допускать прерывание при первом входе в критическую секциию одного из процессов
#define FALSE 0
#define TRUE 1
#define N 2 // количество процессов

int turn; // чья очередь
int interested[N];   // изначально значения равны 0

void enter_region(int process) // номер процесса - 0 или 1
{
  int other;
  other = 1 - process;    // точка 1
  interested[process] = TRUE;  // точка 2 (заинтересованный процесс)
  turn = process;         // точка 3
  while(turn == process && interested[other] == TRUE); // активное ожидание
}

void leave_region(int process) // номер процесса - 0 или 1
{
  interested[process] = 0; // признак выхода из критической секции
}
Итак, turn и interested[] являются глобальными переменными, прочие
переменные - локальные.
Случай 1: переключение процессов возникло в точке 1
Процесс 0 Процесс 1
other = 1
Прерывание процесса 0, переключение на процесс 1

other = 0, interested[1] = TRUE, turn = 1
Проверка условий turn == 1 (true) и interested[0] == TRUE (false)
Вход в критическую секцию
Прерывание процесса 1, переключение на процесс 0
interested[0] = TRUE, turn = 0
Проверка условий turn == 0 (true) и interested[1] == TRUE (true)
Вход в бесконечный цикл, переключение на процесс 1
Процесс 1 выходит из критической секции и устанавливает interested[1] = FALSE
Прерывание процесса 1, переключение на процесс 0
Проверка условий turn == 0 (true) и interested[1] == TRUE (false)
Вход в критическую секцию


Случай 2: переключение процессов возникло в точке 2
Процесс 0 Процесс 1
other = 1, interested[0] = TRUE
Прерывание процесса 0, переключение на процесс 1
other = 0, interested[1] = 1, turn = 1
Проверка условий turn == 1 (true) и interested[0] == TRUE (true)
Вход в бесконечный цикл, переключение на процесс 0
turn = 0
Проверка условий turn == 0 (true) и interested[1] == 1 (true)
Вход в бесконечный цикл, преключение на процесс 1

Проверка условий turn == 1 (false) и interested[0] == 1 (true)
Вход в критическую секцию
Прерывание процесса 1, переключение на процесс 0
Проверка условий turn == 0 (true) и interested[1] == 1 (true)
Вход в бесконечный цикл, преключение на процесс 1

Процесс 1 выходит из критической секции и устанавливает interested[1] = FALSE
Прерывание процесса 1, переключение на процесс 0
Проверка условий turn == 0 (true) и interested[1] == 1 (false)
Вход в критическую секцию

Случай 3: переключение процессов возникло в точке 3
Процесс 0 Процесс 1

other = 1, interested[0] = TRUE, turn = 0
Прерывание процесса 0, переключение на процесс
other = 0, interested[1] = TRUE, turn = 1
Проверка условий turn == 1 (true) и interested[0] == TRUE (true)
Вход в бесконечный цикл, преключение на процесс 0
Проверка условий turn == 0 (false) и interested[1] == TRUE (true)
Вход в критическую секцию
Прерывание процесса 0, переключение на процесс
Проверка условий turn == 1 (true) and interested[0] == TRUE (true)
Вход в бесконечный цикл, преключение на процесс 0
Процесс 0 выходит из критической секции и устанавливает interested[0] = FALSE
Прерывание процесса 0, переключение на процесс
Проверка условий turn == 1 (true) и interested[0] == TRUE (false)
Вход в критическую секцию


Продолжение следует...