Linux多线程编程实验:哲学家进餐问题

85 阅读9分钟

Linux多线程编程实验:哲学家进餐问题

一、实验内容

利用POSIX API在Linux系统上编写应用程序,通过多线程和互斥量机制/POSIX信号量机制实现哲学家进餐问题(哲学家的数量可以通过简单的配置进行修改)

•首先通过阻塞加锁的操作方式实现哲学家之间(线程)对筷子(临界资源)的互斥访问,观察程序运行一段时间以后会出现的死锁状态。如果未出现死锁状态的可以通过修改哲学家数量以及修改延时设置来增大出现死锁的机率。

•将互斥量的加锁操作从阻塞方式修改为非阻塞方式,通过让权等待的思想预防死锁状态的出现。 请你针对这个实验,编写该实验的实验指导书,包括实验目的、实验原理和具体实验步骤。

二、实验目的

1.      掌握POSIX线程编程:熟悉pthread库的使用,学会创建和管理多线程程序

2.      理解同步机制:掌握互斥量(mutex)和信号量(semaphore)的使用方法

3.      分析死锁问题:通过实际编程观察和分析死锁现象的产生原因

4.      学习死锁预防:通过非阻塞加锁和让权等待策略预防死锁

5.      提高并发编程能力:培养设计和实现安全并发程序的能力

三、实验原理

3.1 哲学家进餐问题描述

哲学家进餐问题是计算机科学中著名的同步问题,用于说明并发系统中的死锁和资源竞争问题。

问题描述

l  N个哲学家围坐在圆桌旁

l  每个哲学家面前有一盘食物

l  相邻两个哲学家之间有一支筷子,共N支筷子

l  哲学家的行为:思考 → 拿起筷子 → 进餐 → 放下筷子 → 继续思考

l  进餐时需要同时拿起左右两支筷子

3.2 死锁产生的原因

死锁的四个必要条件

1.      互斥条件:筷子一次只能被一个哲学家使用

2.      请求和保持条件:哲学家拿到一支筷子后,等待另一支筷子

3.      不可剥夺条件:筷子不能被强制从哲学家手中夺走

4.      环路等待条件:形成环形等待链

死锁场景:所有哲学家同时拿起左边的筷子,然后等待右边的筷子,形成环路等待。

3.3 POSIX线程同步机制

3.3.1 互斥量(Mutex)

l  pthread_mutex_t:互斥量数据类型

l  pthread_mutex_lock() :阻塞加锁

l  pthread_mutex_trylock() :非阻塞加锁

l  pthread_mutex_unlock() :解锁

3.3.2 信号量(Semaphore)

l  sem_t:信号量数据类型

l  sem_wait() :P操作(阻塞)

l  sem_trywait() :非阻塞P操作

l  sem_post() :V操作

3.4 死锁预防策略

1.      破坏请求和保持条件:使用非阻塞加锁,获取不到资源时释放已持有的资源

2.      破坏环路等待条件:为资源编号,按序申请资源

3.      让权等待:获取不到所需资源时,主动让出CPU并稍后重试

四、实验环境

l  操作系统:Linux(Ubuntu/CentOS/Debian等),我是在mac电脑上编译和运行的

l  编译器:GCC

l  线程库:pthread

l  同步库:pthread mutex、POSIX semaphore

五、实验步骤

步骤1:准备实验环境

1.      创建实验目录:

mkdir dining_philosophers

cd dining_philosophers

1.      检查编译环境:

gcc --version

结果如图所示:

image.png

步骤2:实现阻塞版本(会产生死锁)

创建文件 philosophers_blocking.c:

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <unistd.h>

#include <time.h>

 

#define MAX_PHILOSOPHERS 10

#define THINKING_TIME 1

#define EATING_TIME 2

 

int num_philosophers = 5;  // 可配置的哲学家数量

pthread_mutex_t chopsticks[MAX_PHILOSOPHERS];

int eating_count[MAX_PHILOSOPHERS] = {0};

 

void* philosopher(void* arg) {

    int id = (int)arg;

    int left = id;

    int right = (id + 1) % num_philosophers;

   

    while (1) {

        // 思考

        printf("哲学家 %d 正在思考...\n", id);

        sleep(THINKING_TIME);

        

        // 尝试拿起左边的筷子

        printf("哲学家 %d 尝试拿起左边筷子 %d\n", id, left);

        pthread_mutex_lock(&chopsticks[left]);

        printf("哲学家 %d 拿起了左边筷子 %d\n", id, left);

        

        // 短暂延时,增加死锁概率

        usleep(100000);

        

        // 尝试拿起右边的筷子

        printf("哲学家 %d 尝试拿起右边筷子 %d\n", id, right);

        pthread_mutex_lock(&chopsticks[right]);

        printf("哲学家 %d 拿起了右边筷子 %d\n", id, right);

        

        // 进餐

        printf("哲学家 %d 开始进餐(第 %d 次)\n", id, ++eating_count[id]);

        sleep(EATING_TIME);

        

        // 放下筷子

        pthread_mutex_unlock(&chopsticks[right]);

        pthread_mutex_unlock(&chopsticks[left]);

        printf("哲学家 %d 放下筷子,进餐完毕\n", id);

    }

    return NULL;

}

 

int main(int argc, char* argv[]) {

    if (argc > 1) {

        num_philosophers = atoi(argv[1]);

        if (num_philosophers > MAX_PHILOSOPHERS) {

            num_philosophers = MAX_PHILOSOPHERS;

        }

    }

   

    printf("开始哲学家进餐问题模拟(阻塞版本)\n");

    printf("哲学家数量: %d\n", num_philosophers);

   

    // 初始化互斥量

    for (int i = 0; i < num_philosophers; i++) {

        pthread_mutex_init(&chopsticks[i], NULL);

    }

   

    // 创建线程

    pthread_t threads[MAX_PHILOSOPHERS];

    int ids[MAX_PHILOSOPHERS];

   

    for (int i = 0; i < num_philosophers; i++) {

        ids[i] = i;

        pthread_create(&threads[i], NULL, philosopher, &ids[i]);

    }

   

    // 等待线程结束(实际上会死锁)

    for (int i = 0; i < num_philosophers; i++) {

        pthread_join(threads[i], NULL);

    }

   

    // 清理资源

    for (int i = 0; i < num_philosophers; i++) {

        pthread_mutex_destroy(&chopsticks[i]);

    }

   

    return 0;

}

步骤3:编译和运行阻塞版本

gcc -o philosophers_blocking philosophers_blocking.c -lpthread

./philosophers_blocking 5

观察结果

l  运行程序,观察输出

l  程序会在某个时刻停止输出,进入死锁状态

l  使用 Ctrl+C 终止程序

结果如图所示:

image.png

image.png

步骤4:实现非阻塞版本(预防死锁)

创建文件 philosophers_nonblocking.c:

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <unistd.h>

#include <time.h>

#include <errno.h>

 

#define MAX_PHILOSOPHERS 10

#define THINKING_TIME 1

#define EATING_TIME 2

#define RETRY_DELAY 50000  // 50ms

 

int num_philosophers = 5;

pthread_mutex_t chopsticks[MAX_PHILOSOPHERS];

int eating_count[MAX_PHILOSOPHERS] = {0};

int retry_count[MAX_PHILOSOPHERS] = {0};

 

void* philosopher(void* arg) {

    int id = (int)arg;

    int left = id;

    int right = (id + 1) % num_philosophers;

   

    while (1) {

        // 思考

        printf("哲学家 %d 正在思考...\n", id);

        sleep(THINKING_TIME);

        

        // 尝试同时获取两支筷子

        int got_chopsticks = 0;

        while (!got_chopsticks) {

            // 尝试拿起左边的筷子

            if (pthread_mutex_trylock(&chopsticks[left]) == 0) {

                printf("哲学家 %d 拿起了左边筷子 %d\n", id, left);

                

                // 尝试拿起右边的筷子

                if (pthread_mutex_trylock(&chopsticks[right]) == 0) {

                    printf("哲学家 %d 拿起了右边筷子 %d\n", id, right);

                    got_chopsticks = 1;

                } else {

                    // 获取右边筷子失败,释放左边筷子

                    pthread_mutex_unlock(&chopsticks[left]);

                    printf("哲学家 %d 无法获取右边筷子,释放左边筷子\n", id);

                    retry_count[id]++;

                }

            } else {

                printf("哲学家 %d 无法获取左边筷子\n", id);

                retry_count[id]++;

            }

            

            if (!got_chopsticks) {

                // 让权等待

                printf("哲学家 %d 让权等待(重试次数: %d)\n", id, retry_count[id]);

                usleep(RETRY_DELAY);

            }

        }

        

        // 进餐

        printf("哲学家 %d 开始进餐(第 %d 次)\n", id, ++eating_count[id]);

        sleep(EATING_TIME);

        

        // 放下筷子

        pthread_mutex_unlock(&chopsticks[right]);

        pthread_mutex_unlock(&chopsticks[left]);

        printf("哲学家 %d 放下筷子,进餐完毕\n", id);

    }

    return NULL;

}

 

int main(int argc, char* argv[]) {

    if (argc > 1) {

        num_philosophers = atoi(argv[1]);

        if (num_philosophers > MAX_PHILOSOPHERS) {

            num_philosophers = MAX_PHILOSOPHERS;

        }

    }

   

    printf("开始哲学家进餐问题模拟(非阻塞版本)\n");

    printf("哲学家数量: %d\n", num_philosophers);

   

    // 初始化互斥量

    for (int i = 0; i < num_philosophers; i++) {

        pthread_mutex_init(&chopsticks[i], NULL);

    }

   

    // 创建线程

    pthread_t threads[MAX_PHILOSOPHERS];

    int ids[MAX_PHILOSOPHERS];

   

    for (int i = 0; i < num_philosophers; i++) {

        ids[i] = i;

        pthread_create(&threads[i], NULL, philosopher, &ids[i]);

    }

   

    // 运行一段时间后显示统计信息

    sleep(30);

    printf("\n=== 30秒统计信息 ===\n");

    for (int i = 0; i < num_philosophers; i++) {

        printf("哲学家 %d: 进餐次数 = %d, 重试次数 = %d\n", 

               i, eating_count[i], retry_count[i]);

    }

   

    // 清理资源

    for (int i = 0; i < num_philosophers; i++) {

        pthread_cancel(threads[i]);

    }

   

    for (int i = 0; i < num_philosophers; i++) {

        pthread_mutex_destroy(&chopsticks[i]);

    }

   

    return 0;

}

步骤5:编译和运行非阻塞版本

gcc -o philosophers_nonblocking philosophers_nonblocking.c -lpthread

./philosophers_nonblocking 5

运行结果如图所示:

image.png

image.png

步骤6:使用信号量实现

创建文件 philosophers_semaphore.c:

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <semaphore.h>

#include <unistd.h>

#include <time.h>

 

#define MAX_PHILOSOPHERS 10

#define THINKING_TIME 1

#define EATING_TIME 2

 

int num_philosophers = 5;

sem_t chopsticks[MAX_PHILOSOPHERS];

sem_t dining_room;  // 限制同时进餐的人数

int eating_count[MAX_PHILOSOPHERS] = {0};

 

void* philosopher(void* arg) {

    int id = (int)arg;

    int left = id;

    int right = (id + 1) % num_philosophers;

   

    while (1) {

        // 思考

        printf("哲学家 %d 正在思考...\n", id);

        sleep(THINKING_TIME);

        

        // 进入餐厅(最多允许n-1个人同时进餐)

        sem_wait(&dining_room);

        

        // 拿起筷子

        sem_wait(&chopsticks[left]);

        printf("哲学家 %d 拿起了左边筷子 %d\n", id, left);

        

        sem_wait(&chopsticks[right]);

        printf("哲学家 %d 拿起了右边筷子 %d\n", id, right);

        

        // 进餐

        printf("哲学家 %d 开始进餐(第 %d 次)\n", id, ++eating_count[id]);

        sleep(EATING_TIME);

        

        // 放下筷子

        sem_post(&chopsticks[right]);

        sem_post(&chopsticks[left]);

        printf("哲学家 %d 放下筷子,进餐完毕\n", id);

        

        // 离开餐厅

        sem_post(&dining_room);

    }

    return NULL;

}

 

int main(int argc, char* argv[]) {

    if (argc > 1) {

        num_philosophers = atoi(argv[1]);

        if (num_philosophers > MAX_PHILOSOPHERS) {

            num_philosophers = MAX_PHILOSOPHERS;

        }

    }

   

    printf("开始哲学家进餐问题模拟(信号量版本)\n");

    printf("哲学家数量: %d\n", num_philosophers);

   

    // 初始化信号量

    for (int i = 0; i < num_philosophers; i++) {

        sem_init(&chopsticks[i], 0, 1);  // 每支筷子初始化为1

    }

    sem_init(&dining_room, 0, num_philosophers - 1);  // 最多n-1人同时进餐

   

    // 创建线程

    pthread_t threads[MAX_PHILOSOPHERS];

    int ids[MAX_PHILOSOPHERS];

   

    for (int i = 0; i < num_philosophers; i++) {

        ids[i] = i;

        pthread_create(&threads[i], NULL, philosopher, &ids[i]);

    }

   

    // 运行一段时间后显示统计信息

    sleep(30);

    printf("\n=== 30秒统计信息 ===\n");

    for (int i = 0; i < num_philosophers; i++) {

        printf("哲学家 %d: 进餐次数 = %d\n", i, eating_count[i]);

    }

   

    // 清理资源

    for (int i = 0; i < num_philosophers; i++) {

        pthread_cancel(threads[i]);

    }

   

    for (int i = 0; i < num_philosophers; i++) {

        sem_destroy(&chopsticks[i]);

    }

    sem_destroy(&dining_room);

   

    return 0;

}

步骤7:编译和运行信号量版本

gcc -o philosophers_semaphore philosophers_semaphore.c -lpthread

./philosophers_semaphore 5

运行结果如图所示:

image.png

提问:

问题1:请解释死锁的四个必要条件,并说明哲学家进餐问题是如何满足这四个条件的。

标准答案

  • 互斥条件:筷子一次只能被一个哲学家使用
  • 请求和保持条件:哲学家拿到一支筷子后,继续等待另一支筷子
  • 不可剥夺条件:筷子不能被强制从哲学家手中夺走
  • 环路等待条件:所有哲学家都拿左筷子等右筷子,形成环路

问题2:为什么pthread_mutex_trylock()能够预防死锁?请从死锁四个条件的角度分析。

标准答案: pthread_mutex_trylock()破坏了"请求和保持"条件。当无法获取所需资源时,线程会释放已持有的资源,而不是继续保持并等待。

问题3:解释"让权等待"策略的原理,为什么要在重试前加入延迟?

标准答案: 让权等待是指当无法获取资源时,主动释放CPU让其他线程执行。加入延迟是为了:

  • 避免所有线程同时重新竞争
  • 给其他线程完成操作的机会
  • 减少系统资源消耗

问题4:在信号量实现中,为什么要限制同时进餐的哲学家数量为n-1?

标准答案: 限制为n-1可以确保至少有一个哲学家能够获得两支筷子,从而避免所有哲学家都持有一支筷子的死锁状态。