哲学家进餐问题

        本篇文章我们使用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: 哲学家进餐(最终代码)

        另外两种解决死锁的方法读者可自行了解. 

哲学家进餐问题涉及到一个经典的多线程同步问题,即多个哲学家围坐在一张圆桌旁,每个哲学家面前放着一碗饭和一只叉子。哲学家思考和进餐,思考时需要两只叉子,进餐时需要拿起两只叉子。问题的关键在于如何保证每个哲学家都能得到他所需要的两只叉子而不产生死锁。 以下是三种解决哲学家进餐问题代码示例: 代码1: 1. 创建一个共享的锁数组,每个元素代表一只叉子; 2. 每个哲学家线程首先尝试获取自己左边的叉子,然后再尝试获取自己右边的叉子; 3. 若成功获取到两只叉子,则可以进餐,进餐完成后释放两只叉子; 4. 若未能同时获取到两只叉子,则释放已经获取到的叉子,进入等待状态,等待其他线程释放叉子。 代码2: 1. 创建一个共享的信号量数组,每个元素代表一只叉子; 2. 每个哲学家线程首先尝试获取自己左边的叉子,然后再尝试获取自己右边的叉子; 3. 若成功获取到两只叉子,则可以进餐,进餐完成后释放两只叉子; 4. 若未能同时获取到两只叉子,则释放已经获取到的叉子,进入等待状态,等待其他线程释放叉子。 代码3: 1. 创建一个共享的计数器,代表可用的叉子数量; 2. 每个哲学家线程首先尝试获取自己左边的叉子,然后再尝试获取自己右边的叉子; 3. 若成功获取到两只叉子,则可以进餐,进餐完成后释放两只叉子,并将计数器加1; 4. 若未能同时获取到两只叉子,则释放已经获取到的叉子,进入等待状态,等待其他线程释放叉子并将计数器加1。 这三种代码的目标都是确保每个哲学家都能获取到所需要的两只叉子,避免死锁情况的发生。具体使用哪种代码取决于实际需求和环境。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值