哲学家就餐问题与python解决方案

引言

本篇是之前复习完哲学家就餐问题后,最近又回过头去感觉可以整理下思路,又在实验楼找到了python相关实验,故想在这里总结一下当前问题的方案。

问题描述

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
在这里插入图片描述

问题分析

系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系。即每个哲学家进程需要同时持有两个临界资源才能开始吃饭。该问题只会出现三种情况:

  1. 每个大哲都拿着左手边的的叉子,等着右手边有叉子好吃火锅
  2. 每个大哲都拿着右手边的的叉子,等着左手边有叉子好吃火锅
  3. 有人做适当牺牲,先让别人进行吃饭,吃完再轮到自己

那这里情况1与情况2被称作死锁(deadlock),指当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时的状况。

情况3是死锁的变种——活锁(livelock),活锁与死锁类似,都是需要获取对方的资源而等待导致停滞不前,唯一的区别是,察觉到停滞不前时会先释放自己的资源,过一段时间再进行获取资源的尝试,但如果双方的操作同步了的话,仍然会导致停滞不前的情况。

这个过程我们可以通过python代码模拟为:

#-*- coding:utf-8 -*-

import threading
from time import sleep
import os, random

#设置为2更容易重现死锁
numPhilosophers = numForks = 2

class Philosopher(threading.Thread):
   def __init__(self, index):
       threading.Thread.__init__(self)
       self.index = index
       self.leftFork = forks[self.index]
       self.rightFork = forks[(self.index + 1) % numForks]

   def run(self):
       while True:
           self.leftFork.pickup()
           self.rightFork.pickup()
           self.dining()
           self.leftFork.putdown()
           self.rightFork.putdown()
           self.thinking()
           
   def dining(self):
       print("Philosopher", self.index, " starts to eat.")
       sleep(random.uniform(1,3)/1000)
       print("Philosopher", self.index, " finishes eating and leaves to think.")

   def thinking(self):
       sleep(random.uniform(1,3)/1000)

class Fork():
   def __init__(self, index):
       self.index = index
       self._lock = threading.Lock()
       
   def pickup(self):
       self._lock.acquire()

   def putdown(self):
       self._lock.release()

if __name__ == '__main__':
   #创建叉子与哲学家实例
   forks = [Fork(idx) for idx in range(numForks)]
   philosophers = [Philosopher(idx) for idx in range(numPhilosophers)]

   #开启所有的哲学家线程
   for philosopher in philosophers:
       philosopher.start()

   # 方便 CTRL + C 退出程序
   try:
       while True: sleep(0.1)
   except Exception as e:
       raise e

事件截图为:
在这里插入图片描述

问题解法

服务生解法

引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。服务生要保证桌子上始终有一只及以上的餐叉。

其实也就是OS中的信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1} 用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家 i 左边的筷子编号为 i,右边的筷子编号为 (i+1)%5。

伪码为:

semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex = 1; //互斥地取筷子
Pi (){ //i号哲学家的进程
while(1){
P(mutex);
P(chopstick[i]); //拿左
P(chopstick[(i+1)%5]); //拿右
V(mutex);
吃饭…
V(chopstick[i]); //放左
V(chopstick[(i+1)%5]); //放右
思考…
}
}

python的代码为:

# -*- coding:utf-8 -*-

import threading
from time import sleep
import os, random


numPhilosophers = numForks = 5


class Philosopher(threading.Thread):
    def __init__(self, index):
        threading.Thread.__init__(self)
        self.index = index
        self.leftFork = forks[self.index]
        self.rightFork = forks[(self.index + 1) % numForks]

    def run(self):
        while True:
            if waiter.serve(self):
                self.dining()
                waiter.clean(self)
            self.thinking()

    def dining(self):
        print("Philosopher", self.index, " starts to eat.")
        sleep(random.uniform(1, 3) / 1000)
        print("Philosopher", self.index, " finishes eating and leaves to think.")

    def thinking(self):
        sleep(random.uniform(1, 3) / 1000)


class Fork():
    def __init__(self, index):
        self.index = index
        self._lock = threading.Lock()

    def pickup(self):
        self._lock.acquire()

    def putdown(self):
        self._lock.release()



class Waiter():
    def __init__(self):
        self.forks = [Fork(idx) for idx in range(numForks)]
        # 最开始餐叉还没有被分配给任何人,所以全部 False
        self.forks_using = [False] * numForks

    # 如果哲学家的左右餐叉都是空闲状态,就为这位哲学家服务提供餐叉
    def serve(self, philor):
        if not self.forks_using[philor.leftFork.index] and not self.forks_using[philor.rightFork.index]:
            self.forks_using[philor.leftFork.index] = True
            self.forks_using[philor.rightFork.index] = True
            self.forks[philor.leftFork.index].pickup()
            self.forks[philor.rightFork.index].pickup()
            return True
        else:
            return False

    #哲学家用餐完毕后,清理并回收餐叉
    def clean(self, philor):
        self.forks[philor.leftFork.index].putdown()
        self.forks[philor.rightFork.index].putdown()
        self.forks_using[philor.leftFork.index] = False
        self.forks_using[philor.rightFork.index]= False

if __name__ == '__main__':
    # 创建叉子与哲学家实例
    waiter = Waiter()
    forks = [Fork(idx) for idx in range(numForks)]
    philosophers = [Philosopher(idx) for idx in range(numPhilosophers)]

    # 开启所有的哲学家线程
    for philosopher in philosophers:
        philosopher.start()

    # 方便 CTRL + C 退出程序
    try:
        while True: sleep(0.1)
    except Exception as e:
        raise e

资源分级解法

为资源分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放,而且保证不会有两个无关资源同时被同一项工作所需要。在哲学家就餐问题中,资源(餐叉)按照某种规则编号为1至5,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。这样就能保证编号最大的餐叉不会被竞争了。

我们只需要在模版的基础上修改哲学家的拿餐叉策略就可以了:

# -*- coding:utf-8 -*-

import threading
from time import sleep
import os, random


numPhilosophers = numForks = 5

class Philosopher(threading.Thread):
    def __init__(self, index):
        threading.Thread.__init__(self)
        self.index = index
        self.leftFork = forks[self.index]
        self.rightFork = forks[(self.index + 1) % numForks]

    def run(self):
        while True:
            if self.leftFork.index > self.rightFork.index:
                firstFork = self.rightFork
                secondFork = self.leftFork
            else:
                firstFork = self.leftFork
                secondFork = self.rightFork

            firstFork.pickup()
            secondFork.pickup()

            self.dining()

            secondFork.putdown()
            firstFork.putdown()

            self.thinking()

    def dining(self):
        print("Philosopher", self.index, " starts to eat.")
        sleep(random.uniform(1, 3) / 1000)
        print("Philosopher", self.index, " finishes eating and leaves to think.")

    def thinking(self):
        sleep(random.uniform(1, 3) / 1000)


class Fork():
    def __init__(self, index):
        self.index = index
        self._lock = threading.Lock()

    def pickup(self):
        self._lock.acquire()

    def putdown(self):
        self._lock.release()


if __name__ == '__main__':
    # 创建叉子与哲学家实例
    forks = [Fork(idx) for idx in range(numForks)]
    philosophers = [Philosopher(idx) for idx in range(numPhilosophers)]

    # 开启所有的哲学家线程
    for philosopher in philosophers:
        philosopher.start()

    # 方便 CTRL + C 退出程序
    try:
        while True: sleep(0.1)
    except Exception as e:
        raise e

尽管资源分级能避免死锁,但这种策略并不总是实用的。例如,假设一个工作单元拿着资源3和5,并决定需要资源2,则必须先要释放5,之后释放3,才能得到2,之后必须重新按顺序获取3和5。本来只需要获取2这一个步骤的,现在却需要经过五个步骤了。对需要访问大量数据库记录的计算机程序来说,如果需要先释放高编号的记录才能访问新的记录,那么运行效率就不会高,因此这种方法在这里并不实用。

但这种方法经常是实际计算机科学问题中最实用的解法,通过为分级锁指定常量,强制获得锁的顺序,就可以解决死锁问题。

3. Chandy/Misra解法

1984年,K. Mani Chandy和J. Misra提出了哲学家就餐问题的另一个解法,允许任意的用户(编号P1, …, Pn)争用任意数量的资源。与資源分級解法不同的是,这里编号可以是任意的。

  • 对每一对竞争一个资源的哲学家,新拿一个餐叉,给编号较低的哲学家。每只餐叉都是“干净的”或者“脏的”。最初,所有的餐叉都是脏的。
  • 当一位哲学家要使用资源(也就是要吃东西)时,他必须从与他竞争的邻居那里得到。对每只他当前没有的餐叉,他都发送一个请求。
  • 当拥有餐叉的哲学家收到请求时,如果餐叉是干净的,那么他继续留着,否则就擦干净并交出餐叉。
  • 当某个哲学家吃东西后,他的餐叉就变脏了。如果另一个哲学家之前请求过其中的餐叉,那他就擦干净并交出餐叉。

这样就能保证没吃到面的大哲有着更高的吃面优先级,这个解法允许很大的并行性,适用于任意大的问题。

该解法可以看GitHub中的dining_philosophers 项目,同样是用python写的,因为涉及代码很多,这里就不再转述。

### 回答1: 好的,下面是一个简单的实现: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <time.h> #define N 5 // 哲学家的数量 #define LEFT (i + N - 1) % N // 左邻居 #define RIGHT (i + 1) % N // 右邻居 #define THINKING 0 // 思考 #define HUNGRY 1 // 饥饿 #define EATING 2 // 就餐 pthread_mutex_t mutex; // 互斥锁 pthread_cond_t cond[N]; // 条件变量,表示是否可以拿起叉子 int state[N]; // 哲学家的状态 int eat_count[N]; // 哲学家就餐次数 void *philosopher(void *arg); void thinking(int i); void eating(int i); void take_forks(int i); void put_forks(int i); void test(int i); int main() { int i; pthread_t tid[N]; // 初始化互斥锁和条件变量 pthread_mutex_init(&mutex, NULL); for (i = 0; i < N; i++) { pthread_cond_init(&cond[i], NULL); state[i] = THINKING; // 初始状态为思考 eat_count[i] = 0; // 初始就餐次数为0 } // 创建线程 for (i = 0; i < N; i++) { pthread_create(&tid[i], NULL, philosopher, (void *)i); } // 等待线程结束 for (i = 0; i < N; i++) { pthread_join(tid[i], NULL); } // 销毁互斥锁和条件变量 pthread_mutex_destroy(&mutex); for (i = 0; i < N; i++) { pthread_cond_destroy(&cond[i]); } return 0; } void *philosopher(void *arg) { int i = (int)arg; while (1) { thinking(i); // 思考 take_forks(i); // 拿起叉子 eating(i); // 就餐 put_forks(i); // 放下叉子 } } void thinking(int i) { printf("Philosopher %d is thinking...\n", i); sleep(rand() % 5 + 1); // 休眠1~5秒,模拟思考 } void eating(int i) { printf("Philosopher %d is eating...\n", i); eat_count[i]++; // 就餐次数加1 sleep(rand() % 3 + 1); // 休眠1~3秒,模拟就餐 } void take_forks(int i) { pthread_mutex_lock(&mutex); // 加锁 state[i] = HUNGRY; // 设置状态为饥饿 printf("Philosopher %d is hungry...\n", i); test(i); // 尝试拿起叉子 while (state[i] != EATING) { // 如果无法拿起叉子,则等待条件变量 pthread_cond_wait(&cond[i], &mutex); } pthread_mutex_unlock(&mutex); // 解锁 } void put_forks(int i) { pthread_mutex_lock(&mutex); // 加锁 state[i] = THINKING; // 设置状态为思考 printf("Philosopher %d put down forks...\n", i); test(LEFT); // 测试左邻居是否可以拿起叉子 test(RIGHT); // 测试右邻居是否可以拿起叉子 pthread_mutex_unlock(&mutex); // 解锁 } void test(int i) { if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) { state[i] = EATING; // 设置状态为就餐 printf("Philosopher %d picks up forks...\n", i); pthread_cond_signal(&cond[i]); // 唤醒等待条件变量的线程 } } ``` 该程序中,每个哲学家都是一个线程,使用互斥锁保证线程之间的互斥访问,并使用条件变量来表示是否可以拿起叉子。当一个哲学家饥饿时,首先尝试拿起叉子,如果无法拿起,则等待条件变量,直到其他哲学家放下叉子并唤醒该线程。当一个哲学家就餐完毕后,放下叉子并唤醒左右邻居,让他们有机会拿起叉子就餐。 ### 回答2: Linux哲学家进餐问题是一个经典的并发编程问题,用于展示多线程同步的挑战。问题的背景是五位哲学家坐在一张圆桌周围,每人面前放着一碗米饭,每两个哲学家之间固定有一把餐叉。哲学家可以交替地进行思考和进餐,但是每个哲学家进餐时需要同时拿起他的相邻两个餐叉,且每根餐叉最多只能由一个哲学家持有。如果有两位哲学家同时试图拿起同一根餐叉,那么就会发生死锁。 为了解决这个问题,可以引入一个餐叉管理者(Fork Manager),这个管理者负责对所有的餐叉进行管理和分配。使用互斥锁来保护餐叉的分配过程,每个哲学家进餐时需要先请求两个餐叉的持有权,如果得到了持有权则可以进餐,否则就需要等待。当进餐结束后需要释放这两个餐叉的持有权,以供其他哲学家使用。 可以使用线程来模拟哲学家和餐叉,并使用互斥锁来控制对餐叉的访问。 实现中需要创建五个线程分别表示五位哲学家,每个线程中循环执行思考和进餐的过程。当哲学家进餐时,需要先请求两个餐叉的锁,然后判断是否都得到了锁的持有权,如果得到则可以进餐进餐结束后要释放锁,供其他哲学家使用。 需要注意的是,为了避免死锁,可以约定哲学家都按照相同的顺序请求餐叉,即先请求左手边的餐叉再请求右手边的餐叉,这样就不会发生死锁问题。 要实现这个问题,需要对线程的创建、互斥锁的初始化、请求锁和释放锁的操作进行编程。编写完整的代码实现Linux哲学家进餐问题需要大量的代码,这里无法详细列举。可以通过查阅相关资料如书籍或网络教程来获取更具体的实现细节和示例代码。 ### 回答3: Linux哲学家进餐问题是一个经典的多线程同步问题,其假设有五位哲学家围绕着一张圆桌坐着,每个哲学家面前都放着一只碗和一只叉子。 根据问题的规则,每个哲学家需要交替地进行思考和进餐。思考时,哲学家不需要使用任何资源;而进餐时,哲学家需要同时获取其左右两边的叉子才能开始进餐,并在用餐完毕后释放叉子供其他哲学家使用。 以下是一个简单的代码实现示例: ```python import threading # 创建五只叉子 forks = [threading.Lock() for _ in range(5)] class Philosopher(threading.Thread): def __init__(self, index): threading.Thread.__init__(self) self.index = index def run(self): left_fork = forks[self.index] right_fork = forks[(self.index + 1) % 5] while True: self.think() self.eat(left_fork, right_fork) def think(self): print(f"哲学家{self.index}正在思考...") def eat(self, left_fork, right_fork): left_fork.acquire() right_fork.acquire() print(f"哲学家{self.index}开始进餐...") # 进餐逻辑代码 left_fork.release() right_fork.release() print(f"哲学家{self.index}用餐结束.") # 创建五位哲学家 philosophers = [Philosopher(i) for i in range(5)] # 启动哲学家线程 for p in philosophers: p.start() # 等待所有哲学家完成 for p in philosophers: p.join() ``` 以上代码利用threading模块创建了五个哲学家线程,并通过threading.Lock()对象模拟了五只叉子。每个哲学家的run方法中,通过交替调用think和eat方法实现思考和进餐的交替进行。其中,eat方法使用了acquire和release方法来获取和释放叉子资源。需要注意的是,为避免死锁,哲学家在获取到左边叉子后再去获取右边叉子。 以上代码只是一个简单的实现示例,实际应用中可能还需要考虑更多的细节,如超时处理、死锁检测等。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

submarineas

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值