操作系统 Dining Philosophers Problem 怎么解决?

文章导读
Previous Quiz Next 餐桌哲学家问题 是一个经典的同步问题,它展示了多个进程或实体之间共享资源所面临的挑战。该问题由 Edsger Dijkstra 在 1965 年提出。阅读本章以了解该问题、其条件及其解决方案。
📋 目录
  1. 餐桌哲学家问题详解
  2. 餐桌哲学家问题中的死锁情况
  3. 使用 Semaphores 解决餐桌哲学家问题
  4. 实现
  5. 结论
A A

操作系统 - 餐桌哲学家问题



Previous
Quiz
Next

餐桌哲学家问题 是一个经典的同步问题,它展示了多个进程或实体之间共享资源所面临的挑战。该问题由 Edsger Dijkstra 在 1965 年提出。阅读本章以了解该问题、其条件及其解决方案。

  • 餐桌哲学家问题详解
  • 死锁情况
  • 使用 Semaphores 的解决方案
  • Python、C++ 和 Java 中的实现

餐桌哲学家问题详解

想象有六个哲学家围坐在一张圆桌旁。每位哲学家有两种状态:思考进食。要开始进食,哲学家需要同时拿起叉子勺子。然而,桌子上只有三个叉子和三个勺子,它们被放置在哲学家之间,如下图所示 —

Dining Philosophers Problem

哲学家只能使用紧邻他们的叉子和勺子。我们的挑战是设计一个协议,使哲学家能够进食,而不会遇到死锁或饥饿问题。

餐桌哲学家问题中的死锁情况

现在,想象所有哲学家同时开始进食,每个人先拿起左边的餐具。这将导致死锁情况。每位哲学家都会拿着一个餐具(叉子或勺子),并等待另一个餐具可用。结果是没有人能进食。

P1 -> Picks Spoon on left  ->  Needs Fork on right.(held by P6)
P2 -> Picks Fork on left   ->  Needs Spoon on right.(held by P1)
P3 -> Picks Spoon on left  ->  Needs Fork on right.(held by P2)
P4 -> Picks Fork on left   ->  Needs Spoon on right.(held by P3)
P5 -> Picks Spoon on left  ->  Needs Fork on right.(held by P4)
P6 -> Picks Fork on left   ->  Needs Spoon on right.(held by P5)

Result: Deadlock - No one can eat.

让我们看看如何使用 semaphores 的概念来解决这个问题。

使用 Semaphores 解决餐桌哲学家问题

Semaphores 是用于控制对共享资源访问的同步原语。在这种情况下,我们可以使用 semaphores 来管理叉子和勺子的可用性。

算法

  • 叉子和勺子都用二进制 semaphores 表示,初始化为 1(可用)。
  • 哲学家在进食前必须获取叉子和勺子。
  • 进食后,哲学家释放叉子和勺子。

哲学家行为的伪代码可以表示如下 —

semaphore fork[3] = {1, 1, 1}; // 三个叉子
semaphore spoon[3] = {1, 1, 1}; // 三个勺子

void philosopher(int i) {
    while (true) {
        think(); // 哲学家在思考

        // 拿起叉子和勺子
        wait(fork[i]);       // 拿起叉子
        wait(spoon[i]);      // 拿起勺子

        eat(); // 哲学家在进食

        // 放下叉子和勺子
        signal(spoon[i]);    // 放下勺子
        signal(fork[i]);     // 放下叉子
    }
}

在该算法中,每位哲学家先思考,然后尝试拿起左边的叉子和勺子。进食后,他们放下两个餐具。这种方法确保没有两位哲学家能同时持有同一个餐具,从而防止死锁。

尽管如此,仍然存在死锁的可能性。如果所有哲学家同时拿起左边的餐具,他们都会等待右边的餐具,从而导致死锁。为了防止这种情况,我们可以引入一条新规则:只有 N-1 位哲学家可以同时开始进食,其中 N 是哲学家的总数。这可以防止循环等待条件,从而避免死锁。

实现

以下是用信号量在 C++/Python 和 Java 中实现 Dining Philosophers Problem 的简单示例:

注意: 我们将叉子和勺子视为同一实体(存储在 forks 数组中),并确保哲学家必须获取左右两侧的餐具才能开始进食。这将简化实现。

Python C++ Java
import threading
import time
import random

N = 6
forks = [threading.Lock() for _ in range(N)]
waiter = threading.Semaphore(N - 1)
stop_event = threading.Event()     # <-- 全局停止标志

def philosopher(i):
    left = forks[i]
    right = forks[(i + 1) % N]
    while not stop_event.is_set():   # <-- 停止条件
        print(f"Philosopher {i} is thinking.")
        time.sleep(random.uniform(0.2, 0.6))   # 为了清晰起见,缩短时间

        if stop_event.is_set():
            break

        if not waiter.acquire(timeout=0.5):     # 避免永久阻塞
            continue

        if not left.acquire(timeout=0.5):
            waiter.release()
            continue

        if not right.acquire(timeout=0.5):
            left.release()
            waiter.release()
            continue

        try:
            print(f"Philosopher {i} is eating.")
            time.sleep(random.uniform(0.2, 0.6))
        finally:
            right.release()
            left.release()
            waiter.release()

# Start threads
threads = []
for i in range(N):
    t = threading.Thread(target=philosopher, args=(i,))
    threads.append(t)
    t.start()

# Let the simulation run for 5 seconds
time.sleep(5)
stop_event.set()      # signal all philosophers to stop

# Wait for threads to finish
for t in threads:
    t.join()

print("Simulation finished.")

上述代码的输出将类似于以下内容 −

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 5 is thinking.
Philosopher 2 is eating.
Philosopher 5 is eating.
Philosopher 2 is thinking.
.......
Stimulation finished.
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <random>

using namespace std::chrono_literals;

class Semaphore {
public:
    explicit Semaphore(int initial) : count(initial) {}
    // try to acquire within timeout; returns true if acquired
    bool try_acquire_for(std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lk(m);
        if (!cv.wait_for(lk, timeout, [&]{ return count > 0; })) return false;
        --count;
        return true;
    }
    void release() {
        std::lock_guard<std::mutex> lk(m);
        ++count;
        cv.notify_one();
    }
private:
    std::mutex m;
    std::condition_variable cv;
    int count;
};

int main() {
    const int N = 6;
    std::vector<std::timed_mutex> forks(N);
    Semaphore waiter(N - 1);
    std::atomic<bool> stop_flag{false};

    auto philosopher = [&](int i) {
        // per-thread RNG
        std::mt19937 rng(std::random_device{}() ^ (i << 8));
        std::uniform_int_distribution<int> think_ms(200, 600);
        std::uniform_int_distribution<int> eat_ms(200, 600);

        while (!stop_flag.load(std::memory_order_relaxed)) {
            std::cout << "Philosopher " << i << " is thinking." << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(think_ms(rng)));

            // request permission from the waiter (timeout so we can exit)
            if (!waiter.try_acquire_for(500ms)) continue;

            bool left_locked = false, right_locked = false;
            // try acquire left fork
            left_locked = forks[i].try_lock_for(500ms);
            if (!left_locked) {
                waiter.release();
                continue;
            }
            // try acquire right fork
            right_locked = forks[(i + 1) % N].try_lock_for(500ms);
            if (!right_locked) {
                forks[i].unlock();
                waiter.release();
                continue;
            }

            // Both forks acquired
            try {
                std::cout << "Philosopher " << i << " is eating." << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(eat_ms(rng)));
            } catch (...) {
                // not expected, but ensure release
            }

            // release forks and waiter
            forks[(i + 1) % N].unlock();
            forks[i].unlock();
            waiter.release();
        }
        // exiting
        //std::cout << "Philosopher " << i << " exiting.\n";
    };

    std::vector<std::thread> threads;
    threads.reserve(N);
    for (int i = 0; i < N; ++i) threads.emplace_back(philosopher, i);

    // run for 5 seconds
    std::this_thread::sleep_for(5s);
    stop_flag.store(true);

    for (auto &t : threads) if (t.joinable()) t.join();

    std::cout << "Simulation finished.\n";
    return 0;
}

上述代码的输出将类似于以下内容 −

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 5 is thinking.
Philosopher 3 is eating.
Philosopher 1 is eating.
Philosopher 3 is thinking.
.......
Simulation finished.
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock; 
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Random;

public class Dining {
    static final int N = 6;

    public static void main(String[] args) throws InterruptedException {
        final ReentrantLock[] forks = new ReentrantLock[N];
        for (int i = 0; i < N; ++i) forks[i] = new ReentrantLock();

        final Semaphore waiter = new Semaphore(N - 1);
        final AtomicBoolean stop = new AtomicBoolean(false);

        Thread[] threads = new Thread[N];

        for (int i = 0; i < N; ++i) {
            final int id = i;

            threads[i] = new Thread(() -> {
                Random rng = new Random(System.nanoTime() ^ (id << 8));

                while (!stop.get()) {
                    System.out.println("Philosopher " + id + " is thinking.");
                    try {
                        Thread.sleep(200 + rng.nextInt(401)); // 200–600 ms
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }

                    boolean gotWaiter = false;
                    boolean leftLocked = false;
                    boolean rightLocked = false;

                    try {
                        gotWaiter = waiter.tryAcquire(500, TimeUnit.MILLISECONDS);
                        if (!gotWaiter) continue;

                        leftLocked = forks[id].tryLock(500, TimeUnit.MILLISECONDS);
                        if (!leftLocked) continue;

                        rightLocked = forks[(id + 1) % N].tryLock(500, TimeUnit.MILLISECONDS);
                        if (!rightLocked) continue;

                        System.out.println("Philosopher " + id + " is eating.");
                        Thread.sleep(200 + rng.nextInt(401));

                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;

                    } finally {
                        if (rightLocked) forks[(id + 1) % N].unlock();
                        if (leftLocked) forks[id].unlock();
                        if (gotWaiter) waiter.release();
                    }
                }
            });

            threads[i].start();
        }

        Thread.sleep(5000);
        stop.set(true);

        for (Thread t : threads) t.join();

        System.out.println("Simulation finished.");
    }
}

上述代码的输出将类似于以下内容 −

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 5 is thinking.
Philosopher 4 is eating.
Philosopher 2 is eating.
Philosopher 4 is thinking.
.......
Simulation finished.

结论

Dining Philosophers Problem(用餐哲学家问题)是并发编程中管理资源共享和同步的经典示例。使用信号量解决这个问题的思路是,确保没有两个哲学家同时持有同一个餐具。我们还引入了一个服务员来限制同时尝试进餐的哲学家数量。这可以防止循环等待,从而避免死锁。