Опубликован: 28.10.2009 | Доступ: свободный | Студентов: 516 / 40 | Оценка: 4.67 / 4.39 | Длительность: 20:33:00
Самостоятельная работа 4:

Отладка параллельной программы с использованием Intel Thread Checker

< Лекция 7 || Самостоятельная работа 4: 123456 || Лекция 8 >

11.2.3. Задача об обедающих философах

На примере данной классической задачи мы продемонстрируем еще одну типичную ошибку - тупик ( deadlock ), ее диагностику при помощи ITC и один из способов устранения.

11.2.3.1. Постановка задачи

Приведем вольную формулировку задачи Дейкстры: за круглым столом заседают 5 философов. Напротив каждого из них стоит блюдо со спагетти. Между каждыми двумя соседями расположена одна вилка. Философ может находиться в одном из двух состояний: ест, размышляет. При еде философу нужны 2 вилки (левая и правая). Стратегия поведения философа следующая: он берет левую вилку (если она свободна) и затем, дождавшись правой вилки, начинает есть. Поев, он освобождает вилки в обратном порядке. Реализовать симулятор, демонстрирующий заседание философов.

11.2.3.2. Параллельная реализация, вариант 1

Реализуем требуемый симулятор на основе потоков Windows Threads.

Идея реализации состоит в следующем: главный поток создает дополнительные потоки в соответствии с количеством философов, запускает их и переходит в бесконечный цикл. Каждый из дополнительных потоков реализует поведение философа. При этом вилки предлагается моделировать при помощи мьютексов, обеспечивая тем самым процедуру "захвата вилки" философом. Функция потока будет принимать в качестве параметров структуру, содержащую мьютексы, соответствующие левой и правой вилкам, а также порядковый номер философа.

Сделаем следующие объявления:

// Количество философов
const unsigned int n = 5;

// Структура - описание философа 
typedef struct 
{ 
  int iID; 							// Номер философа
  HANDLE hMyObjects[2];					// Мьютексы (вилки)
} THREADCONTROLBLOCK, *PTHREADCONTROLBLOCK;

Приведем функцию потока. Используем функцию WaitForSingleObject для ожидания освобождения ресурса (мьютекса).

long WINAPI ThreadRoutine(long lParam)
{ 
  PTHREADCONTROLBLOCK pcb=(PTHREADCONTROLBLOCK)lParam;
  while (TRUE)
  {
    WaitForSingleObject(pcb->hMyObjects[0],INFINITE);
    WaitForSingleObject(pcb->hMyObjects[1],INFINITE);
    printf("Eating: Philosopher %d \n",pcb->iID);
    ReleaseMutex(pcb->hMyObjects[1]);
    ReleaseMutex(pcb->hMyObjects[0]);
  };
  return (0);
}

Тогда функция main будет выглядеть так:

int main()
{
  HANDLE hMutexes[n];
  THREADCONTROLBLOCK tcb[n];
  int iThreadID;

  for (int i = 0; i < n; i++)
    hMutexes[i] = CreateMutex(NULL, FALSE, NULL);

  for (int i = 0; i < n; i++)
  {
    tcb[i].iID = i+1;
    tcb[i].hMyObjects[0] = hMutexes[i % n];
    tcb[i].hMyObjects[1] = hMutexes[(i+1) % n];
    CloseHandle(CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadRoutine,
      (void *)&tcb[i],0,LPDWORD(&iThreadID)));
   }
   while(TRUE);
   return(0);
}
11.2.3.3. Анализ реализации 1

Запустим программу на выполнение несколько раз.

Задача об обедающих философах - результаты запуска параллельной реализации 1

увеличить изображение
Рис. 11.16. Задача об обедающих философах - результаты запуска параллельной реализации 1

Результаты будут варьироваться от запуска к запуску. Единственное, что их объединяет, - неизменное зависание в некоторый момент. Попробуем разобраться, в чем дело. Прибегнем к помощи Intel Thread Checker. Как видно из рисунка, ITC сгенерировал 5 диагностических сообщений, представляющих для нас интерес. Каждое из сообщений соответствует одному из созданных нами потоков и "говорит" о наличии тупика.

Диагностика ITC в задаче об обедающих философах (параллельная реализация 1)

увеличить изображение
Рис. 11.17. Диагностика ITC в задаче об обедающих философах (параллельная реализация 1)
11.2.3.4. Параллельная реализация, вариант 2

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

Одним из возможных способов решения проблемы является изменение модели поведения философа. К примеру, можно наделить его обязанностью брать вилки одновременно, лишь тогда, когда обе они свободны. Изменения в программной реализации будут минимальны - достаточно заменить 2 вызова функции WaitForSingleObject на 1 вызов функции WaitForMultipleObjects для одновременного захвата мьютексов, соответствующим обеим вилкам.

#include <stdio.h>
#include <windows.h>

// Количество философов
const unsigned int n = 5;

typedef struct { 
  int iID;
  HANDLE hMyObjects[2];
} THREADCONTROLBLOCK, *PTHREADCONTROLBLOCK;

long WINAPI ThreadRoutine(long lParam) { 
  PTHREADCONTROLBLOCK pcb=(PTHREADCONTROLBLOCK)lParam;
  while (TRUE) {
    WaitForMultipleObjects(2, pcb->hMyObjects, TRUE, INFINITE);
    printf("Eating: Philosopher %d \n",pcb->iID);
    ReleaseMutex(pcb->hMyObjects[1]);
    ReleaseMutex(pcb->hMyObjects[0]);
  };
return (0);
} 

int main() {
   HANDLE hMutexes[n];
   THREADCONTROLBLOCK tcb[n];
   int iThreadID;

   for (int i = 0; i < n; i++)
     hMutexes[i] = CreateMutex(NULL,FALSE,NULL);

   for (int i = 0; i < n; i++) {
     tcb[i].iID = i+1;
     tcb[i].hMyObjects[0] = hMutexes[i % n];
     tcb[i].hMyObjects[1] = hMutexes[(i+1) % n];
     CloseHandle(CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadRoutine,
                         (void *)&tcb[i],0,LPDWORD(&iThreadID)));
   }
   while(TRUE);
   return(0);
}
11.2.3.5. Анализ реализации 2

Тестовые запуски подтверждают наши ожидания - программа перестала зависать. ITC также не делает по представленному выше коду никаких замечаний.

< Лекция 7 || Самостоятельная работа 4: 123456 || Лекция 8 >