stringtranslate.com

Проблема обедающих философов

Иллюстрация проблемы обедающих философов. У каждого философа есть миска спагетти, и он может достать две вилки.

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

Первоначально он был сформулирован в 1965 году Эдсгером Дейкстрой как студенческий экзамен, представленный в виде конкуренции компьютеров за доступ к периферийным устройствам для ленточных накопителей . Вскоре после этого Тони Хоар придал проблеме ее нынешнюю форму. [1] [2] [3] [4]

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

Пять философов обедают вместе за одним столом. У каждого философа на столе есть своя тарелка. Между каждой тарелкой есть вилка. Подаваемое блюдо представляет собой своего рода спагетти , которые нужно есть двумя вилками. Каждый философ может только попеременно думать и есть. Более того, философ может есть спагетти только тогда, когда у него есть и левая, и правая вилки. Таким образом, две вилки будут доступны только тогда, когда два их ближайших соседа думают, а не едят. После того, как отдельный философ заканчивает есть, он откладывает обе вилки. Проблема в том, как разработать режим ( параллельный алгоритм), при котором ни один философ не будет голодать; т. е . каждый может вечно продолжать чередовать еду и мышление, предполагая, что ни один философ не может знать, когда другие могут захотеть есть или думать (проблема неполной информации ).

Проблемы

Задача была разработана, чтобы проиллюстрировать, как избежать тупика — состояния системы, в котором прогресс невозможен. Чтобы увидеть, что правильное решение этой проблемы неочевидно, рассмотрим предложение, в котором каждому философу предлагается вести себя следующим образом:

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

Нехватка ресурсов , взаимное исключение и блокировка в реальном времени — это другие типы проблем последовательности и доступа.

Решения

Этих четырех условий достаточно для возникновения тупика: взаимное исключение (ни одна вилка не может быть использована одновременно несколькими философами), удержание ресурса (философы держат вилку, ожидая второй), отсутствие вытеснения (ни один философ не может взять вилку). от другого) и круговое ожидание (каждый философ может ждать философа слева от него). Решение должно свести на нет хотя бы одно из этих четырех условий. На практике отрицание взаимного исключения или невытеснения каким-то образом может дать действенное решение, но большинство теоретических подходов предполагают, что эти предположения не подлежат обсуждению, вместо этого они атакуют удержание ресурсов или циклическое ожидание (часто и то, и другое).

Решение Дейкстры

Решение Дейкстры сводит на нет владение ресурсами; философы атомарно берут обе вилки или ждут, никогда не удерживая ровно одну вилку за пределами критической секции. Для этого решение Дейкстры использует один мьютекс , один семафор на каждого философа и одну переменную состояния на каждого философа. Это решение более сложное, чем решение с иерархией ресурсов. [5] [4] Это версия решения Дейкстры для C++20 с изменениями Таненбаума:

#include <chrono> #include <iostream> #include <mutex> #include <random> #include <semaphore> #include <thread>      constexpr const size_t N = 5 ; // количество философов (и вилок) enum class State { THINKING = 0 , // философ ДУМАЕТ ГОЛОДНО = 1 , // философ пытается достать вилки EATING = 2 , // философ ЕДИТ };                     size_t inline left ( size_t i ) { // номер левого соседа философа i, для которого доступны обе вилки return ( i - 1 + N ) % N ; // N добавляется для случая, когда i - 1 отрицательно }               size_t inline right ( size_t i ) { // номер правого соседа философа i, для которого доступны обе вилки return ( i + 1 ) % N ; }            Состояние штата [ N ]; // массив для отслеживания состояния Both_forks_available каждого  std :: мьютекс критический_регион_mtx ; // взаимное исключение критических регионов для // (взятия и опускания вилок) std :: mutex output_mtx ; // для синхронизированного cout (вывод статуса ДУМАНИЕ/ГОЛОД/ЕДЕНИЕ)    // массив бинарных семафоров, по одному семафору на каждого философа. // Полученный семафор означает, что философ i приобрел (заблокировал) две вилки std :: binary_semaphore Both_forks_available [ N ] { std :: binary_semaphore { 0 }, std :: binary_semaphore { 0 }, std :: binary_semaphore { 0 }, std :: binary_semaphore {0} , std :: binary_semaphore {0} } ;      size_t my_rand ( size_t min , size_t max ) { static std :: mt19937 rnd ( std :: time ( nullptr )); return std :: uniform_int_distribution <> ( min , max )( rnd ); }           void test ( size_t i ) // если философ i голоден и оба соседа не едят, то едят { // i: номер философа от 0 до N-1 if ( state [ i ] == State :: HUNGRY && state [ left ( i )] != Состояние :: ЕДА && состояние [ right ( i )] != Состояние :: ЕДА ) { состояние [ i ] = Состояние :: ЕДА ; обе_вилки_доступны [ я ]. выпускать (); // вилки для этого сеанса еды больше не нужны } }                         void think ( size_t я ) { size_t продолжительность = my_rand ( 400 , 800 ); { std :: lock_guard < std :: mutex > lk ( output_mtx ); // критическая секция для непрерывной печати std :: cout << i << " думает " << длительность << "ms \n " ; } std :: this_thread :: sleep_for ( std :: хроно :: миллисекунды ( длительность )); }                       void take_forks ( size_t i ) { { std :: lock_guard < std :: mutex > lk { Critical_region_mtx }; // вход в критическое состояние региона [ i ] = State :: HUNGRY ; // записываем тот факт, что философ i — State::HUNGRY { std :: lock_guard < std :: mutex > lk ( output_mtx ); // критическая секция для непрерывной печати std :: cout << " \t\t " << i << " is State::HUNGRY \n " ; } Тест ( я ); // попытаемся получить (разрешение на) 2 форка } // выходим из критической области Both_forks_available [ i ]. приобретать (); // блокируем, если форки не были получены }                            void eat ( size_t я ) { size_t продолжительность = my_rand ( 400 , 800 ); { std :: lock_guard < std :: mutex > lk ( output_mtx ); // критическая секция для непрерывной печати std :: cout << " \t\t\t\t " << i << " съедает " << длительность << "ms \n " ; } std :: this_thread :: sleep_for ( std :: хроно :: миллисекунды ( длительность )); }                        void put_forks ( size_t i ) { std :: lock_guard < std :: mutex > lk { critical_region_mtx }; // вход в состояние критической области [ i ] = State :: THINKING ; // философ завершил тест State::EATING ( left ( i ) ); // проверяем, может ли левый сосед съесть test ( right ( i )); // проверяем, может ли правый сосед теперь есть // выходим из критической области, выйдя из функции }                 void философ ( size_t i ) { while ( true ) { // повторять вечно, think ( i ); // философ — это State::THINKING take_forks ( i ); // получаем две вилки или блокируем eat ( i ); // ням-ням, спагетти put_forks ( i ); // кладем обе вилки обратно на стол и проверяем, могут ли соседи есть } }                 int main () { std :: cout << "dp_14 \n " ;      std :: jthread t0 ([ & ] { философ ( 0 ); }); // [&] означает каждую переменную вне последующей лямбды std :: jthread t1 ([ & ] { философ ( 1 ); }); // захватывается по ссылке std :: jthread t2 ([ & ] { философ ( 2 ); }); std :: jthread t3 ([ & ] { философ ( 3 ); }); std :: jthread t4 ([ & ] { философ ( 4 ); }); }                          

Функция test() и ее использование в take_forks() и put_forks() делают решение Дейкстры свободным от взаимоблокировок.

Решение иерархии ресурсов

Это решение исключает циклическое ожидание, назначая частичный порядок ресурсам (в данном случае ветвям) и устанавливает соглашение, согласно которому все ресурсы будут запрашиваться по порядку и что никакие два ресурса, не связанные порядком, никогда не будут использоваться одним единица работы одновременно. Здесь ресурсы (вилки) будут пронумерованы от 1 до 5, и каждая единица работы (философ) всегда сначала выбирает вилку с меньшим номером, а затем вилку с большим номером из двух вилок, которые он планирует использовать. Порядок, в котором каждый философ кладет вилки, не имеет значения. В этом случае, если четыре из пяти философов одновременно возьмут вилки с меньшим номером, на столе останется только вилка с наибольшим номером, поэтому пятый философ не сможет взять вилку. Более того, только один философ будет иметь доступ к той вилке с наибольшим номером, поэтому он сможет есть, используя две вилки. Интуитивно это можно представить себе как наличие за столом одного философа-левши, который – в отличие от всех других философов – первым берет вилку слева.

Хотя решение иерархии ресурсов позволяет избежать взаимоблокировок, оно не всегда практично, особенно когда список необходимых ресурсов заранее не известен полностью. Например, если единица работы содержит ресурсы 3 и 5, а затем определяет, что ей нужен ресурс 2, она должна освободить 5, затем 3, прежде чем получить 2, а затем она должна повторно получить 3 и 5 в этом порядке. Компьютерные программы, которые обращаются к большому количеству записей базы данных, не будут работать эффективно, если от них потребуется освободить все записи с более высокими номерами перед доступом к новой записи, что делает этот метод непрактичным для этой цели. [2]

Решение с иерархией ресурсов несправедливо . Если философ 1 медленно берет вилку, а философ 2 быстро думает и снова поднимает вилки, то философ 1 никогда не сможет взять обе вилки. Справедливое решение должно гарантировать, что каждый философ в конечном итоге поест, независимо от того, насколько медленно этот философ движется относительно других.

Следующий исходный код представляет собой реализацию C++11 решения иерархии ресурсов для пяти философов. Функция Sleep_for() имитирует время, обычно затрачиваемое на бизнес-логику. [6]

Для GCC: скомпилировать с помощью

g++  src.cpp  -std = С++11  -lpthread
#include <iostream> #include <chrono> #include <mutex> #include <thread> #include <random> #include <ctime>      использование пространства имен std ;  int myrand ( int min , int max ) { static mt19937 rnd ( time ( nullptr )); вернуть униформу_int_distribution <> ( мин , макс )( rnd ); }          void философ ( int ph , mutex & ma , mutex & mb , mutex & mo ) { for (;;) { // предотвращаем завершение потока int period = myrand ( 200 , 800 ); { // Block { } ограничивает область действия блокировки lock_guard < mutex > gmo ( mo ); cout << ph << " думает " << длительность << "ms \n " ; } this_thread :: Sleep_for ( хроно :: миллисекунды ( длительность )); { lock_guard < мьютекс > gmo ( mo ); cout << " \t\t " << ph << " голоден \n " ; } lock_guard < мьютекс > gma ( ма ); // Sleep_for() Здесь можно добавить задержку перед поиском второй вилки, но это не обязательно. lock_guard < мьютекс > gmb ( mb ); продолжительность = миранд ( 200 , 800 ); { lock_guard < мьютекс > gmo ( mo ); cout << " \t\t\t\t " << ph << " ест " << длительность << "ms \n " ; } this_thread :: Sleep_for ( хроно :: миллисекунды ( длительность )); } }                                              int main () { cout << "обед Философов C++11 с иерархией ресурсов \n " ; мьютекс m1 , m2 , m3 , m4 , m5 ; // 5 вилок — это 5 мьютексов mutex mo ; // для корректного вывода // 5 философов — это 5 потоков thread t1 ([ & ] { философ ( 1 , m1 , m2 , mo );}); нить t2 ([ & ] { философ ( 2 , m2 , m3 , mo );}); нить t3 ([ & ] { философ ( 3 , m3 , m4 , mo );}); нить t4 ([ & ] { философ ( 4 , m4 , m5 , mo );}); thread t5 ([ & ] { философ ( 5 , m1 , m5 , mo );}); // Форсируем иерархию ресурсов t1 . присоединиться (); // предотвращаем завершение потоков t2 . присоединиться (); т3 . присоединиться (); т4 . присоединиться (); т5 . присоединиться (); }                                                     

Решение арбитра

Другой подход состоит в том, чтобы гарантировать, что философ может взять только обе вилки или не взять ни одной, введя арбитра вместо циклического ожидания, например, официанта. Чтобы взять вилки, философ должен спросить разрешения у официанта. Официант дает разрешение только одному философу за раз, пока тот не возьмет в руки обе вилки. Всегда можно положить вилку. Официант может быть реализован как мьютекс. Помимо введения новой центральной сущности (официанта), этот подход может привести к уменьшению параллелизма: если философ ест, а один из его соседей запрашивает вилки, все остальные философы должны ждать, пока этот запрос не будет выполнен, даже если вилки для них все еще доступны.

Параллельная разработка алгоритмов

Ограничение количества посетителей за столом

Решение, предложенное Уильямом Столлингсом [7], состоит в том, чтобы позволить максимум n-1 философам сесть за стол в любое время. Последнему философу пришлось бы ждать (например, с помощью семафора), пока кто-нибудь закончит обедать, прежде чем он «сядет» и запросит доступ к любой вилке. Это сводит на нет циклическое ожидание, гарантируя, что хотя бы один философ всегда может получить обе вилки, что позволяет системе развиваться.

Решение Чанди/Мисра

В 1984 году К. Мани Чанди и Дж. Мисра [8] предложили другое решение проблемы обедающих философов, позволяющее произвольным агентам (с номерами P 1 , ..., P n ) бороться за произвольное количество ресурсов, в отличие от Решение Дейкстры. Он также полностью распределен и не требует никаких центральных полномочий после инициализации. Однако это нарушает требование, чтобы «философы не разговаривали друг с другом» (из-за сообщений-запросов).

  1. Для каждой пары философов, конкурирующих за ресурс, создайте форк и передайте его философу с меньшим идентификатором ( n для агента P n ). Каждая вилка может быть грязной или чистой. Изначально все вилки грязные.
  2. Когда философ хочет использовать набор ресурсов ( т. е . поесть), он должен получить вилки от своих конкурирующих соседей. На все такие развилки, которых у философа нет, он отправляет сообщение-запрос.
  3. Когда философ с вилкой получает сообщение с просьбой, он оставляет вилку чистой, но отдает ее, когда она грязная. Если философ посылает вилку, он перед этим очищает вилку.
  4. После того, как философ закончил есть, все его вилки становятся грязными. Если другой философ ранее запросил одну из вилок, философ, который только что закончил есть, очищает вилку и отправляет ее.

Это решение также обеспечивает высокую степень параллелизма и решит сколь угодно большую проблему.

Это также решает проблему голодания. Этикетки «чистый/грязный» служат способом отдать предпочтение наиболее «голодным» процессам и поставить в невыгодное положение процессы, которые только что «съели». Их решение можно сравнить с решением, в котором философам не разрешается есть дважды подряд, не позволяя другим пользоваться вилками между ними. Решение Чанди и Мисры более гибкое, но в нем есть элемент, тяготеющий в этом направлении.

В своем анализе они выводят систему уровней предпочтений на основе распределения вилок и их чистых/грязных состояний. Они показывают, что эта система может описывать ориентированный ациклический граф , и если это так, то операции в их протоколе не могут превратить этот граф в циклический. Это гарантирует, что тупиковая ситуация не возникнет, поскольку исключает циклическое ожидание. Однако если система инициализирована в идеально симметричном состоянии, как все философы, держащие левую вилку, то граф с самого начала является циклическим, и их решение не может предотвратить тупик. Инициализация системы так, чтобы у философов с более низкими идентификаторами были грязные вилки, гарантирует, что граф изначально ацикличен.

Смотрите также

Рекомендации

  1. ^ Дейкстра, Эдсгер В. EWD-1000 (PDF) . Архив Э. В. Дейкстры. Центр американской истории Техасского университета в Остине .(транскрипция)
  2. ^ аб Дж. Диас; И. Рамос (1981). Формализация концепций программирования: Международный коллоквиум, Пенискола, Испания, 19–25 апреля 1981 г. Труды. Биркхойзер. стр. 323, 326. ISBN. 978-3-540-10699-9.
  3. ^ Хоар, ЦАР (2004) [первоначально опубликовано в 1985 году издательством Prentice Hall International]. «Обмен последовательными процессами» (PDF) . используя csp.com.
  4. ^ ab Таненбаум, Эндрю С. (2006), Операционные системы - проектирование и реализация, 3-е издание [Глава: 2.3.1 Проблема обедающих философов] , Pearson Education, Inc.
  5. ^ Дейкстра, Эдсгер В. EWD-310 (PDF) . Архив Э. В. Дейкстры. Центр американской истории Техасского университета в Остине .(транскрипция)
  6. ^ Таненбаум, Эндрю С. (2006), Операционные системы - проектирование и реализация, 3-е издание [Глава: 3.3.5 Предотвращение тупиков] , Pearson Education, Inc.
  7. ^ Столлингс, Уильям (2018). Операционные системы: внутреннее устройство и принципы проектирования (9-е изд.). Харлоу, Эссекс, Англия: Пирсон . п. 310. ИСБН 978-1-292-21429-0. ОКЛК  1009868379.
  8. ^ Чанди, КМ; Мисра, Дж. (1984). Проблема пьющих философов. Транзакции ACM в языках и системах программирования.

Библиография

Внешние ссылки