Дедлок и проблема обедающих философов

Теги: deadlock, дедлок, взаимная блокировка, проблема обедающих философов



Deadlock

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

Как пример дедлока рассмотрим задачу обедающих философов. Эта задача была придумана Дейкстрой и сформулирована в современном виде Хоаром для описания типичных проблем многопоточной среды.

За круглым столом сидят философы (для определённости их пять), пронумерованные против часовой стрелки. На столе лежат 5 вилок. Каждый философ должен взять две вилки (левую от себя и правую от себя), пообедать и положить их обратно. Проблема в том, что философы берут вилки по очереди, поэтому после того, как один взял левую вилку, правая, к примеру, уже может находиться в руках второго философа. Тогда первый вынужден будет ждать, когда второй пообедает.

Пусть первый уже взял левую вилку. Второй, третий, четвёртый и пятый поступили также. Левая вилка у второго философа является правой у первого, поэтому первый не может взять правую. Философ будет бесконечно ожидать правую вилку, также будут ожидать и все остальные философы. Мы получим дедлок, в котором пять потоков взаимно ожидают разблокировки ресурсов. Очевидно, что из этой ситуации есть выход, и даже не один. Для начала реализуем эту ситуацию, а потом найдём из неё какой-нибудь выход.

Объявим структуру «философ», которая будет хранить имя философа и номера вилок, которые он может взять.

#define PHT_SIZE 5

typedef struct philosopher_tag {
	const char *name;
	unsigned left_fork;
	unsigned right_fork;
} philosopher_t;

Далее структура «стол», которая состоит их массива вилок. В качестве вилки будет выступать мьютекс. Блокировка мьютекса означает «взять вилку», а разблокировка «положить её обратно».

typedef struct table_tag {
	pthread_mutex_t forks[PHT_SIZE];
} table_t;

Кроме того, для передачи этих параметров в поток объединим их в структуру

typedef struct philosopher_args_tag {
	const philosopher_t *philosopher;
	const table_t *table;
} philosopher_args_t;

Две вспомогательные функции инициализации

void init_philosopher(philosopher_t *philosopher, const char *name, unsigned left_fork, unsigned right_fork) {
	philosopher->name = name;
	philosopher->left_fork = left_fork;
	philosopher->right_fork = right_fork;
}

void init_table(table_t *table) {
	size_t i;
	for (i = 0; i < PHT_SIZE; i++) {
		pthread_mutex_init(&table->forks[i], NULL);
	}
}

И наша основная функция, eat, которая моделирует обед

void* eat(void *args) {
	philosopher_args_t *arg = (philosopher_args_t*) args;
	const philosopher_t *philosopher = arg->philosopher;
	const table_t *table = arg->table;

	printf("%s started dinner\n", philosopher->name);

	pthread_mutex_lock(&table->forks[philosopher->left_fork]);
	pthread_mutex_lock(&table->forks[philosopher->right_fork]);

	printf("%s is eating\n", philosopher->name);

	pthread_mutex_unlock(&table->forks[philosopher->right_fork]);
	pthread_mutex_unlock(&table->forks[philosopher->left_fork]);

	printf("%s finished dinner\n", philosopher->name);
}

Теперь осталось самое малое – инициализировать 5 потоков и запустить их.

void main() {
	pthread_t threads[PHT_SIZE];

	philosopher_t philosophers[PHT_SIZE];
	philosopher_args_t arguments[PHT_SIZE];
	table_t table;

	size_t i;

	init_table(&table);

	init_philosopher(&philosophers[0], "Alice", 0, 1);
	init_philosopher(&philosophers[1], "Bob", 1, 2);
	init_philosopher(&philosophers[2], "Clark", 2, 3);
	init_philosopher(&philosophers[3], "Denis", 3, 4);
	init_philosopher(&philosophers[4], "Eugin", 4, 0);

	for (i = 0; i < PHT_SIZE; i++) {
		arguments[i].philosopher = &philosophers[i];
		arguments[i].table = &table;
	}

	for (i = 0; i < PHT_SIZE; i++) {
		pthread_create(&threads[i], NULL, eat, &arguments[i]);
	}

	for (i = 0; i < PHT_SIZE; i++) {
		pthread_join(threads[i], NULL);
	}

	wait();
}

Запускаем, и… никакого дедлока нет. Дело в том, что запуск потока операция небыстрая. Поэтому первый запущенный философ успевает поесть и отдать вилки. Для эмуляции дедлока замедлим процесс подъёма вилки со стола: добавим sleep

pthread_mutex_lock(&table->forks[philosopher->left_fork]);
Sleep(1000);
pthread_mutex_lock(&table->forks[philosopher->right_fork]);	

printf("%s is eating\n", philosopher->name);

pthread_mutex_unlock(&table->forks[philosopher->right_fork]);
pthread_mutex_unlock(&table->forks[philosopher->left_fork]);

Тепеь мы получили состояние, в котором пять потоков ждут разблокировки ресурсов.

Какое может быть решение? Рассмотрим самое простое. Наша проблема заключается в том, что «взять вилки» - это не атомарная операция. Она состоит из двух операций – «взять левую» и «взять правую». Для того, чтобы никто больше не мог брать вилки, пока один человек берёт вилки, добавим ещё один мьютекс, который будет блокировать взятие вилок:

pthread_mutex_t entry_point = PTHREAD_MUTEX_INITIALIZER;
...
pthread_mutex_lock(&entry_point);
pthread_mutex_lock(&table->forks[philosopher->left_fork]);
Sleep(1000);
pthread_mutex_lock(&table->forks[philosopher->right_fork]);	
pthread_mutex_unlock(&entry_point);

printf("%s is eating\n", philosopher->name);

pthread_mutex_unlock(&table->forks[philosopher->right_fork]);
pthread_mutex_unlock(&table->forks[philosopher->left_fork]);

А теперь весь код. Пусть философы едят бесконечно и ждут случайное время

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>

#ifdef WIN32
    #include <conio.h>
    #include <Windows.h>
    #define Sleep(X) Sleep(X)
    #define wait() _getch()
#else
    #include <unistd.h>
    #define Sleep(X) sleep(X)
    #define wait() scanf("1")
#endif

#define PHT_SIZE 5

typedef struct philosopher_tag {
	const char *name;
	unsigned left_fork;
	unsigned right_fork;
} philosopher_t;

typedef struct table_tag {
	pthread_mutex_t forks[PHT_SIZE];
} table_t;

typedef struct philosopher_args_tag {
	const philosopher_t *philosopher;
	const table_t *table;
} philosopher_args_t;

pthread_mutex_t entry_point = PTHREAD_MUTEX_INITIALIZER;

void init_philosopher(philosopher_t *philosopher, 
					  const char *name, 
					  unsigned left_fork,
					  unsigned right_fork) {
	philosopher->name = name;
	philosopher->left_fork = left_fork;
	philosopher->right_fork = right_fork;
}

void init_table(table_t *table) {
	size_t i;
	for (i = 0; i < PHT_SIZE; i++) {
		pthread_mutex_init(&table->forks[i], NULL);
	}
}

void* eat(void *args) {
	philosopher_args_t *arg = (philosopher_args_t*) args;
	const philosopher_t *philosopher = arg->philosopher;
	const table_t *table = arg->table;
	unsigned rand;	

	do {
		printf("%s started dinner\n", philosopher->name);

		pthread_mutex_lock(&entry_point);
		pthread_mutex_lock(&table->forks[philosopher->left_fork]);
		rand_s(&rand);
		rand %= 1000;
		Sleep(rand);
		pthread_mutex_lock(&table->forks[philosopher->right_fork]);
		pthread_mutex_unlock(&entry_point);

		printf("%s is eating after %d ms sleep\n", philosopher->name, rand);

		pthread_mutex_unlock(&table->forks[philosopher->right_fork]);
		pthread_mutex_unlock(&table->forks[philosopher->left_fork]);

		printf("%s finished dinner\n", philosopher->name);
	} while (1);
}

void main() {
	pthread_t threads[PHT_SIZE];
	philosopher_t philosophers[PHT_SIZE];
	philosopher_args_t arguments[PHT_SIZE];
	table_t table;
	size_t i;

	init_table(&table);

	init_philosopher(&philosophers[0], "Alice", 0, 1);
	init_philosopher(&philosophers[1], "Bob",   1, 2);
	init_philosopher(&philosophers[2], "Clark", 2, 3);
	init_philosopher(&philosophers[3], "Denis", 3, 4);
	init_philosopher(&philosophers[4], "Eugin", 4, 0);

	for (i = 0; i < PHT_SIZE; i++) {
		arguments[i].philosopher = &philosophers[i];
		arguments[i].table = &table;
	}

	for (i = 0; i < PHT_SIZE; i++) {
		pthread_create(&threads[i], NULL, eat, &arguments[i]);
	}

	for (i = 0; i < PHT_SIZE; i++) {
		pthread_join(threads[i], NULL);
	}
}
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Блокировка чтения записи