1. 概述

在并发编程中,有一个著名的问题叫做 “哲学家进餐问题”(Dining Philosophers Problem),最早由 Edgar Dijkstra 于 1965 年提出。这个问题用于演示 资源竞争(resource contention) 的场景,即多个线程或进程需要共享资源时可能出现的同步问题。

2. 问题描述

问题的设定如下图所示:五位哲学家围坐在一张圆桌旁,每人面前有一碗意大利面,但每个人需要同时拿起左右两边的两把叉子才能开始进餐。

diningphilosophers

他们不会一直吃饭,而是交替进行 思考、饥饿、进餐 三个状态。当处于饥饿状态时,哲学家必须同时拿到左右两边的叉子才能开始吃饭。

3. 问题特点

这个问题的关键在于:

✅ 每个叉子只能被一个哲学家使用
✅ 哲学家只能在拿到左右两个叉子后才能开始吃饭
✅ 吃完后必须释放叉子,供其他哲学家使用
❌ 如果所有哲学家同时尝试拿左边的叉子,就会导致 死锁(deadlock)

Dijkstra 提出用三个状态来描述哲学家的行为:

  • 0:思考(THINKING)
  • 1:饥饿(HUNGRY)
  • 2:吃饭(EATING)

每个哲学家都会在 0 → 1 → 2 → 0 的状态中循环。

4. 解决方案

这个问题有多种解法,我们这里介绍一个基于 Dijkstra 和 Tanenbaum 的经典解决方案。核心思想是引入一个 全局协调机制,比如一个“服务员”来决定谁可以开始吃饭,从而避免死锁。

4.1. 共享资源定义

我们定义如下变量:

function SomeConstants():
    N <- 5
    THINKING <- 0
    HUNGRY <- 1
    EATING <- 2
    LEFT <- (i + N - 1) mod N
    RIGHT <- (i + 1) mod N

同步机制使用一个互斥锁(mutex)和一个信号量数组(每个哲学家一个):

function SynchronizationPrimitives():
    semaphores[N] <- 0
    mutex <- 0

哲学家的主循环如下:

function PhilosopherRoutine(philosophernumber):
    i <- philosophernumber
    while 1:
        think(i)
        takeforks(i)
        eat(i)
        putforks(i)

4.2. 防止死锁

关键在于 只有当左右叉子都可用时,才允许哲学家开始吃饭。我们通过 Check(i) 函数来判断:

function Check(i):
    if state[i] = HUNGRY and state[LEFT] != EATING and state[RIGHT] != EATING:
        state[i] = EATING
        sem_wait(philosopher[i])

当哲学家尝试拿叉子时,会先标记自己为饥饿状态,然后调用 Check(i)

function TakeForks(i):
    mutex.lock()
    state[i] <- HUNGRY
    check(i)
    mutex.unlock()
    sem_wait(semaphores[i])

吃完饭后,哲学家会释放叉子,并通知左右邻居检查是否可以开始吃饭:

function PutForks(philosophernumber):
    i <- philosophernumber
    mutex.lock()
    state[i] <- THINKING
    check(LEFT)
    check(RIGHT)
    mutex.unlock()
    sem_wait(semaphores[i])

4.3. 模拟思考和吃饭

这两个函数用于模拟哲学家的行为,仅用于演示:

function Think():
    duration <- rand(1, 5)
    sleep(duration)
function Eat():
    duration <- rand(1, 3)
    sleep(duration)

5. 可能的问题

虽然这个方案可以避免死锁,但仍有几个潜在问题需要注意:

⚠️ 死锁(Deadlock):如果所有哲学家同时进入饥饿状态并尝试拿左边的叉子,就会陷入死锁。
⚠️ 饥饿(Starvation):某些哲学家可能因为调度策略永远拿不到叉子,导致无法吃饭。

这些问题在并发编程中非常常见,尤其是在资源竞争激烈的场景下。Dijkstra 通过这个模型很好地揭示了现实中操作系统和多线程程序中资源调度的复杂性。

6. 总结

哲学家进餐问题是一个经典的并发编程问题,揭示了资源竞争、死锁和饥饿等核心问题。通过引入一个协调机制(如服务员模型),我们可以有效避免这些问题。

这个模型在实际开发中也有广泛应用,比如线程池、数据库连接池、锁竞争优化等场景。理解这个问题有助于我们更好地设计并发系统。

如需查看 Java 实现,请参考:Java 版哲学家进餐问题实现


原始标题:The Dining Philosophers