Отладка параллельной программы с использованием Intel Thread Checker
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
Запустим программу на выполнение несколько раз.
увеличить изображение
Рис. 11.16. Задача об обедающих философах - результаты запуска параллельной реализации 1
Результаты будут варьироваться от запуска к запуску. Единственное, что их объединяет, - неизменное зависание в некоторый момент. Попробуем разобраться, в чем дело. Прибегнем к помощи Intel Thread Checker. Как видно из рисунка, ITC сгенерировал 5 диагностических сообщений, представляющих для нас интерес. Каждое из сообщений соответствует одному из созданных нами потоков и "говорит" о наличии тупика.
увеличить изображение
Рис. 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 также не делает по представленному выше коду никаких замечаний.

