Отладка параллельной программы с использованием 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 также не делает по представленному выше коду никаких замечаний.