哲学家就餐问题与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写的,因为涉及代码很多,这里就不再转述。

猜你喜欢

转载自blog.csdn.net/submarineas/article/details/126454925