本篇文章我们使用C++探讨一下哲学家进餐问题.
如图为5名哲学家, 桌子上有五根筷子和五碗饭, 每个哲学家都只做两件事: 吃饭和思考. 哲学家思考不需要额外的资源, 只需要动用他们的大脑; 但吃饭不同, 吃饭需要拿起两根筷子才能吃饭. 我们还是先从最简单的情况写出代码, 假设哲学家吃饭不需要筷子, 代码如下:
#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>
/// <summary>
/// 哲学家函数
/// </summary>
void philosopher_fun(int index) {
while (true)
{
printf("哲学家%d进餐\n", index);
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
printf("哲学家%d思考\n", index);
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
}
}
int main()
{
std::thread philosopher_thread0(philosopher_fun, 0); //哲学家线程0
std::thread philosopher_thread1(philosopher_fun, 1); //哲学家线程1
std::thread philosopher_thread2(philosopher_fun, 2); //哲学家线程2
std::thread philosopher_thread3(philosopher_fun, 3); //哲学家线程3
std::thread philosopher_thread4(philosopher_fun, 4); //哲学家线程4
philosopher_thread0.join(); //等待线程结束
philosopher_thread1.join();
philosopher_thread2.join();
philosopher_thread3.join();
philosopher_thread4.join();
return 0;
}
代码1: 哲学家进餐(不需要筷子即可进餐)
现在我们需要给进餐加上一些限制, 只有当哲学家把左右两边的筷子都拿起来时, 才能进餐, 进餐后归还筷子. 很显然, 筷子左右两边的哲学家对这根筷子是互斥访问的, 因此五根筷子的信号量初始值均为1, 这里我们可以用一个长度为5的数组chopstick[]来管理这些筷子, 数组中的元素初始值均为1, 每当哲学家 i 进餐之前, 就去申请他左右两边的筷子, 先申请左边的筷子chopstick[ i ], 若chopstick[ i ] 为1, 则成功申请, 若chopstick[ i ] 为0, 则阻塞当前线程; 再申请右边的筷子chopstick[(i + 1) % 5], 若chopstick[(i + 1) % 5]为1, 则申请成功, 若chopstick[(i + 1) % 5]为0, 则阻塞当前线程. 进餐之后, 先归还左边筷子, 再归还右边筷子. 代码如下:
#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>
/// <summary>
/// 筷子数组
/// </summary>
int chopstick[5] = { 1, 1, 1, 1, 1 };
/// <summary>
/// 定义一个长度为 5 的 mutex 数组
/// </summary>
std::mutex mutexArr[5];
std::condition_variable philosopher_cv; //条件变量, 哲学家线程的管理队列
/// <summary>
/// 哲学家函数
/// </summary>
void philosopher_fun(int index) {
while (true)
{
//申请左手边的筷子
std::unique_lock<std::mutex> left_lock(mutexArr[index]);
philosopher_cv.wait(left_lock, [&]() { return chopstick[index] == 1; }); //若左边的筷子的数量为1, 则当前线程可以继续执行
chopstick[index] = 0;
//申请右手边的筷子
std::unique_lock<std::mutex> right_lock(mutexArr[(index + 1) % 5]);
philosopher_cv.wait(right_lock, [&]() { return chopstick[(index + 1) % 5] == 1; }); //若右边的筷子数量为1, 则当前线程可以继续执行
chopstick[(index + 1) % 5] = 0;
printf("哲学家%d进餐\n", index);
//归还左手边的筷子
chopstick[index] = 1;
philosopher_cv.notify_all();
//归还右手边的筷子
chopstick[(index + 1) % 5] = 1;
philosopher_cv.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
printf("哲学家%d思考\n", index);
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
}
}
int main()
{
std::thread philosopher_thread0(philosopher_fun, 0); //哲学家线程0
std::thread philosopher_thread1(philosopher_fun, 1); //哲学家线程1
std::thread philosopher_thread2(philosopher_fun, 2); //哲学家线程2
std::thread philosopher_thread3(philosopher_fun, 3); //哲学家线程3
std::thread philosopher_thread4(philosopher_fun, 4); //哲学家线程4
philosopher_thread0.join(); //等待线程结束
philosopher_thread1.join();
philosopher_thread2.join();
philosopher_thread3.join();
philosopher_thread4.join();
return 0;
}
代码2: 哲学家进餐(需要筷子方可进餐)
读者可自行尝试运行这段代码, 会发现这段代码的打印输出过了一段时间就停止了, 下面我们就分析一下这种现象产生的原因. 试想一下, 当线程0顺利执行到chopstick[index] = 0; 这句代码之后, 就转而去执行线程1的代码, 线程1也执行到chopstick[index] = 0; 这句代码之后, 就转而去执行线程2的代码; 线程2执行到这一句就转而执行线程3, 线程3执行到这一句就转而执行线程4. 这下每个哲学家都占有了左手边的筷子, 当他们继续执行下去时, 需要申请右手边的筷子, 但自己右手边的筷子被自己右手边的哲学家占有, 所以只能一直等待. 我们称这种现象为死锁.
为防止死锁发生, 我们可对哲学家线程施加一些限制条件. 比如: ①仅当一名哲学家左右两边的筷子都可用时,才允许他抓起筷子; ②至多允许4名哲学家同时进餐; ③对哲学家顺序编号, 要求奇数号哲学家先拿左边的筷子, 然后拿右边的筷子, 而偶数号哲学家刚好相反. 这里我们采用第①种方法, 把拿起左边筷子和拿起右边筷子变成不能被打断的操作即可. 代码如下:
#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>
/// <summary>
/// 筷子数组
/// </summary>
int chopstick[5] = { 1, 1, 1, 1, 1 };
/// <summary>
/// 定义一个长度为 5 的 mutex 数组
/// </summary>
std::mutex mutexArr[5];
std::condition_variable philosopher_cv; //条件变量, 哲学家线程的管理队列
/// <summary>
/// 拿起筷子的互斥量, 初值为1
/// </summary>
int get_cpstk_mutex = 1;
/// <summary>
/// 拿起筷子的互斥信号量
/// </summary>
std::mutex get_cpstk_mtx;
/// <summary>
/// 哲学家函数
/// </summary>
void philosopher_fun(int index) {
while (true)
{
std::unique_lock<std::mutex> lock(get_cpstk_mtx);
philosopher_cv.wait(lock, []() {return get_cpstk_mutex = 1; });
get_cpstk_mutex = 0;
//申请左手边的筷子
std::unique_lock<std::mutex> left_lock(mutexArr[index]);
philosopher_cv.wait(left_lock, [&]() { return chopstick[index] == 1; }); //若左边的筷子的数量为1, 则当前线程可以继续执行
chopstick[index] = 0;
//申请右手边的筷子
std::unique_lock<std::mutex> right_lock(mutexArr[(index + 1) % 5]);
philosopher_cv.wait(right_lock, [&]() { return chopstick[(index + 1) % 5] == 1; }); //若右边的筷子数量为1, 则当前线程可以继续执行
chopstick[(index + 1) % 5] = 0;
get_cpstk_mutex = 1;
philosopher_cv.notify_all();
printf("哲学家%d进餐\n", index);
//归还左手边的筷子
chopstick[index] = 1;
philosopher_cv.notify_all();
//归还右手边的筷子
chopstick[(index + 1) % 5] = 1;
philosopher_cv.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
printf("哲学家%d思考\n", index);
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
}
}
int main()
{
std::thread philosopher_thread0(philosopher_fun, 0); //哲学家线程0
std::thread philosopher_thread1(philosopher_fun, 1); //哲学家线程1
std::thread philosopher_thread2(philosopher_fun, 2); //哲学家线程2
std::thread philosopher_thread3(philosopher_fun, 3); //哲学家线程3
std::thread philosopher_thread4(philosopher_fun, 4); //哲学家线程4
philosopher_thread0.join(); //等待线程结束
philosopher_thread1.join();
philosopher_thread2.join();
philosopher_thread3.join();
philosopher_thread4.join();
return 0;
}
代码3: 哲学家进餐(拿起两边筷子的操作不能被打断)
最终代码如下:
#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>
/// <summary>
/// 筷子数组
/// </summary>
int chopstick[5] = { 1, 1, 1, 1, 1 };
/// <summary>
/// 定义一个长度为 5 的 mutex 数组
/// </summary>
std::mutex mutexArr[5];
std::condition_variable philosopher_cv; //条件变量, 哲学家线程的管理队列
/// <summary>
/// 拿起筷子的互斥量, 初值为1
/// </summary>
int get_cpstk_mutex = 1;
/// <summary>
/// 拿起筷子的互斥信号量
/// </summary>
std::mutex get_cpstk_mtx;
/// <summary>
/// 哲学家函数
/// </summary>
void philosopher_fun(int index) {
while (true)
{
//P(get_cpstk_mtx)
std::unique_lock<std::mutex> lock(get_cpstk_mtx);
philosopher_cv.wait(lock, []() {return get_cpstk_mutex = 1; });
get_cpstk_mutex = 0;
//P(mutexArr[index])
std::unique_lock<std::mutex> left_lock(mutexArr[index]);
philosopher_cv.wait(left_lock, [&]() { return chopstick[index] == 1; }); //若左边的筷子的数量为1, 则当前线程可以继续执行
chopstick[index] = 0;
//P(mutexArr[(index + 1) % 5])
std::unique_lock<std::mutex> right_lock(mutexArr[(index + 1) % 5]);
philosopher_cv.wait(right_lock, [&]() { return chopstick[(index + 1) % 5] == 1; }); //若右边的筷子数量为1, 则当前线程可以继续执行
chopstick[(index + 1) % 5] = 0;
//V(get_cpstk_mtx)
get_cpstk_mutex = 1;
philosopher_cv.notify_all();
//eating()
printf("哲学家%d进餐\n", index);
//V(mutexArr[index])
chopstick[index] = 1;
philosopher_cv.notify_all();
//V(mutexArr[(index + 1) % 5])
chopstick[(index + 1) % 5] = 1;
philosopher_cv.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
//thinking()
printf("哲学家%d思考\n", index);
std::this_thread::sleep_for(std::chrono::seconds(1)); //当前线程阻塞1秒
}
}
int main()
{
std::thread philosopher_thread0(philosopher_fun, 0); //哲学家线程0
std::thread philosopher_thread1(philosopher_fun, 1); //哲学家线程1
std::thread philosopher_thread2(philosopher_fun, 2); //哲学家线程2
std::thread philosopher_thread3(philosopher_fun, 3); //哲学家线程3
std::thread philosopher_thread4(philosopher_fun, 4); //哲学家线程4
philosopher_thread0.join(); //等待线程结束
philosopher_thread1.join();
philosopher_thread2.join();
philosopher_thread3.join();
philosopher_thread4.join();
return 0;
}
代码3: 哲学家进餐(最终代码)
另外两种解决死锁的方法读者可自行了解.