操作系统 - 餐桌哲学家问题
餐桌哲学家问题 是一个经典的同步问题,它展示了多个进程或实体之间共享资源所面临的挑战。该问题由 Edsger Dijkstra 在 1965 年提出。阅读本章以了解该问题、其条件及其解决方案。
- 餐桌哲学家问题详解
- 死锁情况
- 使用 Semaphores 的解决方案
- Python、C++ 和 Java 中的实现
餐桌哲学家问题详解
想象有六个哲学家围坐在一张圆桌旁。每位哲学家有两种状态:思考 和 进食。要开始进食,哲学家需要同时拿起叉子和勺子。然而,桌子上只有三个叉子和三个勺子,它们被放置在哲学家之间,如下图所示 —
哲学家只能使用紧邻他们的叉子和勺子。我们的挑战是设计一个协议,使哲学家能够进食,而不会遇到死锁或饥饿问题。
餐桌哲学家问题中的死锁情况
现在,想象所有哲学家同时开始进食,每个人先拿起左边的餐具。这将导致死锁情况。每位哲学家都会拿着一个餐具(叉子或勺子),并等待另一个餐具可用。结果是没有人能进食。
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 数组中),并确保哲学家必须获取左右两侧的餐具才能开始进食。这将简化实现。
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(用餐哲学家问题)是并发编程中管理资源共享和同步的经典示例。使用信号量解决这个问题的思路是,确保没有两个哲学家同时持有同一个餐具。我们还引入了一个服务员来限制同时尝试进餐的哲学家数量。这可以防止循环等待,从而避免死锁。