以下示例实验程序采用经典的管程概念模拟和实现了哲学家就餐问题。其中仍使用以上介绍的 IPC 机制实现进程的同步与互斥操作。为了利用管程解决死锁问题, 本示例程序利用了 C++语言的机制构造了哲学家管程,管程中的条件变量的构造 利用了 linux IPC 的信号量机制,利用了共享内存表示每个哲学家的当前状态。

dp.h

/*
 * Filename : dp.h
 * copyright : (C) 2006 by zhonghonglie
 * Function : 声明 IPC 机制的函数原型和哲学家管程类
 */
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <sys/wait.h>

using namespace std;

/* 信号量控制用的共同体 */
typedef union semuns { int val; } Sem_uns;

// 哲学家的3个状态(思考、饥饿、就餐)
enum State { thinking, hungry, eating };

// 哲学家管程中使用的信号量类
class Sema {
public:
    Sema(int id);
    ~Sema();
    int down(); // 信号量减1 (P操作)
    int up();   // 信号量加1 (V操作)
private:
    int sem_id; // 信号量标识符
};

// 哲学家管程中使用的锁类
class Lock {
public:
    Lock(Sema *lock);
    ~Lock();
    void close_lock();
    void open_lock();
private:
    Sema *sema; // 锁使用的信号量
};

// 哲学家管程中使用的条件变量类
class Condition {
public:
    Condition(char *st[], Sema *sm);
    ~Condition();
    void Wait(Lock *lock, int i); // 条件变量阻塞操作
    void Signal(int i);           // 条件变量唤醒操作
private:
    Sema *sema;       // 哲学家信号量
    char **state;     // 哲学家当前的状态
};

// 哲学家管程的定义
class dp {
public:
    dp(int rate); // 管程构造函数
    ~dp();
    void pickup(int i);  // 获取筷子
    void putdown(int i); // 放下筷子

    // IPC 辅助函数原型
    int get_ipc_id(char *proc_file, key_t key);
    int set_sem(key_t sem_key, int sem_val, int sem_flag);
    char *set_shm(key_t shm_key, int shm_num, int shm_flag);

private:
    int rate;                  // 控制执行速度
    Lock *lock;                // 控制互斥进入管程的锁
    char *state[5];            // 5个哲学家当前的状态
    Condition *self[5];        // 控制5个哲学家状态的条件变量
};

dp.cc

/*
 * Filename : dp.cc
 * copyright : (C) 2006 by zhonghonglie
 * Function : 哲学家就餐问题的模拟程序
 */
#include "dp.h"

// --- Sema 类实现 ---
Sema::Sema(int id) { sem_id = id; }
Sema::~Sema() { }

int Sema::down() {
    struct sembuf buf;
    buf.sem_op = -1;
    buf.sem_num = 0;
    buf.sem_flg = SEM_UNDO;
    if ((semop(sem_id, &buf, 1)) < 0) {
        perror("down error ");
        exit(EXIT_FAILURE);
    }
    return EXIT_SUCCESS;
}

int Sema::up() {
    Sem_uns arg;
    struct sembuf buf;
    buf.sem_op = 1;
    buf.sem_num = 0;
    buf.sem_flg = SEM_UNDO;
    if ((semop(sem_id, &buf, 1)) < 0) {
        perror("up error ");
        exit(EXIT_FAILURE);
    }
    return EXIT_SUCCESS;
}

// --- Lock 类实现 ---
Lock::Lock(Sema *s) { sema = s; }
Lock::~Lock() { }
void Lock::close_lock() { sema->down(); }
void Lock::open_lock() { sema->up(); }

// --- Condition 类实现 ---
Condition::Condition(char *st[], Sema *sm) {
    state = st;
    sema = sm;
}
Condition::~Condition() { }

void Condition::Wait(Lock *lock, int i) {
    // 如果左右邻居不在吃饭,且自己是饥饿状态,则可以吃饭
    if ((*state[(i + 4) % 5] != eating) &&
        (*state[i] == hungry) &&
        (*state[(i + 1) % 5] != eating)) {
        *state[i] = eating;
    } else {
        cout << "p" << i + 1 << ": " << getpid() << " hungry\n";
        lock->open_lock(); // 释放管程锁,允许别人进入
        sema->down();      // 自己阻塞(没拿到筷子)
        lock->close_lock(); // 被唤醒后重新获取锁
    }
}

void Condition::Signal(int i) {
    // 尝试唤醒邻居
    if ((*state[(i + 4) % 5] != eating) &&
        (*state[i] == hungry) &&
        (*state[(i + 1) % 5] != eating)) {
        sema->up(); // 唤醒
        *state[i] = eating;
    }
}

// --- dp 类实现 (辅助函数) ---
int dp::get_ipc_id(char *proc_file, key_t key) {
#define BUFSZ 256
    FILE *pf;
    int i, j;
    char line[BUFSZ], column[BUFSZ];
    if ((pf = fopen(proc_file, "r")) == NULL) {
        perror("Proc file not open");
        exit(EXIT_FAILURE);
    }
    fgets(line, BUFSZ, pf);
    while (!feof(pf)) {
        i = j = 0;
        fgets(line, BUFSZ, pf);
        while (line[i] == ' ') i++;
        while (line[i] != ' ') column[j++] = line[i++];
        column[j] = '\0';
        if (atoi(column) != key) continue;

        j = 0;
        while (line[i] == ' ') i++;
        while (line[i] != ' ') column[j++] = line[i++];
        column[j] = '\0';
        i = atoi(column);
        fclose(pf);
        return i;
    }
    fclose(pf);
    return -1;
}

int dp::set_sem(key_t sem_key, int sem_val, int sem_flag) {
    int sem_id;
    Sem_uns sem_arg;
    if ((sem_id = get_ipc_id("/proc/sysvipc/sem", sem_key)) < 0) {
        if ((sem_id = semget(sem_key, 1, sem_flag)) < 0) {
            perror("semaphore create error");
            exit(EXIT_FAILURE);
        }
    }
    sem_arg.val = sem_val;
    if (semctl(sem_id, 0, SETVAL, sem_arg) < 0) {
        perror("semaphore set error");
        exit(EXIT_FAILURE);
    }
    return sem_id;
}

char *dp::set_shm(key_t shm_key, int shm_num, int shm_flag) {
    int i, shm_id;
    char *shm_buf;
    if ((shm_id = get_ipc_id("/proc/sysvipc/shm", shm_key)) < 0) {
        if ((shm_id = shmget(shm_key, shm_num, shm_flag)) < 0) {
            perror("shareMemory set error");
            exit(EXIT_FAILURE);
        }
    }
    if ((shm_buf = (char *)shmat(shm_id, 0, 0)) < (char *)0) {
        perror("get shareMemory error");
        exit(EXIT_FAILURE);
    }
    // 如果是新创建的,初始化
    if (shm_id == get_ipc_id("/proc/sysvipc/shm", shm_key)) {
        for (i = 0; i < shm_num; i++) shm_buf[i] = 0;
    }
    return shm_buf;
}

// --- dp 类实现 (核心逻辑) ---
dp::dp(int r) {
    int ipc_flag = IPC_CREAT | 0644;
    int shm_key = 220;
    int shm_num = 1; // 实际上我们需要存5个状态,这里简化了逻辑,实际应分配足够空间
    int sem_key = 120;
    int sem_val = 0;
    int sem_id;
    Sema *sema;

    rate = r;

    // 1. 创建互斥锁信号量 (初值为1)
    if ((sem_id = set_sem(sem_key++, 1, ipc_flag)) < 0) {
        perror("Semaphor create error");
        exit(EXIT_FAILURE);
    }
    sema = new Sema(sem_id);
    lock = new Lock(sema);

    // 2. 为每个哲学家创建共享状态和私有信号量
    // 注意:为了简化,这里假设共享内存足够大,或者每个状态单独分配
    // 原始代码逻辑中 set_shm 分配的大小可能不足以存放5个指针指向的数据,
    // 但在实验环境中我们照搬逻辑。
    for (int i = 0; i < 5; i++) {
        if ((state[i] = (char *)set_shm(shm_key++, shm_num, ipc_flag)) == NULL) {
            perror("Share memory create error");
            exit(EXIT_FAILURE);
        }
        *state[i] = thinking;

        if ((sem_id = set_sem(sem_key++, sem_val, ipc_flag)) < 0) {
            perror("Semaphor create error");
            exit(EXIT_FAILURE);
        }
        sema = new Sema(sem_id);
        self[i] = new Condition(state, sema);
    }
}

dp::~dp() { }

void dp::pickup(int i) {
    lock->close_lock(); // 进入管程
    *state[i] = hungry; // 变为饥饿
    self[i]->Wait(lock, i); // 测试能否进餐
    cout << "p" << i + 1 << ": " << getpid() << " eating\n";
    sleep(rate); // 进餐
    lock->open_lock(); // 离开管程
}

void dp::putdown(int i) {
    int j;
    lock->close_lock(); // 进入管程
    *state[i] = thinking; // 变为思考
    // 唤醒左邻居
    j = (i + 4) % 5;
    self[j]->Signal(j);
    // 唤醒右邻居
    j = (i + 1) % 5;
    self[j]->Signal(j);
    lock->open_lock(); // 离开管程

    cout << "p" << i + 1 << ": " << getpid() << " thinking\n";
    sleep(rate); // 思考
}

// --- 主函数 ---
int main(int argc, char *argv[]) {
    dp *tdp;
    int pid[5];
    int rate;

    rate = (argc > 1) ? atoi(argv[1]) : 3;
    tdp = new dp(rate);

    pid[0] = fork();
    if (pid[0] < 0) { perror("p1 create error"); exit(EXIT_FAILURE); }
    else if (pid[0] == 0) { while(1) { tdp->pickup(0); tdp->putdown(0); } }

    pid[1] = fork();
    if (pid[1] < 0) { perror("p2 create error"); exit(EXIT_FAILURE); }
    else if (pid[1] == 0) { while(1) { tdp->pickup(1); tdp->putdown(1); } }

    pid[2] = fork();
    if (pid[2] < 0) { perror("p3 create error"); exit(EXIT_FAILURE); }
    else if (pid[2] == 0) { while(1) { tdp->pickup(2); tdp->putdown(2); } }

    pid[3] = fork();
    if (pid[3] < 0) { perror("p4 create error"); exit(EXIT_FAILURE); }
    else if (pid[3] == 0) { while(1) { tdp->pickup(3); tdp->putdown(3); } }

    pid[4] = fork();
    if (pid[4] < 0) { perror("p5 create error"); exit(EXIT_FAILURE); }
    else if (pid[4] == 0) { while(1) { tdp->pickup(4); tdp->putdown(4); } }

    // 父进程等待
    for(int i=0; i<5; i++) wait(NULL);
    return 0;
}

编译与运行

  1. 编译:在终端中输入 make 或 gmake
  2. 运行:输入 ./dp 1(参数 1 代表睡眠/思考时间为 1 秒,你可以调整这个数字)。
  3. 终止:程序会无限循环,按 Ctrl+C 终止。

可以看到 5 个哲学家进程在 3 中状态中不断的轮流变换,且连续的 5 个输出中不应有多于 2 个的状态为 eating,同一进程号不应有两个连续的输出。
1. 制造死锁

原理:死锁发生是因为所有哲学家同时拿起了左边的筷子,然后都在等待右边的筷子。这是一种循环等待条件。

修改方法

修改 dp.cc 中的 pickup 函数。我们将移除管程(Monitor)的互斥保护,模拟每个哲学家独立地去拿筷子的情况。

// 修改 pickup 函数,破坏原子性
void dp::pickup(int i) {
    // 1. 模拟拿起左边的筷子 (这里用打印代替,实际应获取信号量)
    cout << "p" << i + 1 << " picked up LEFT fork.\n";
    sleep(1); // 强制延迟,增加死锁概率

    // 2. 模拟拿起右边的筷子
    // 如果所有进程都执行到了这里,大家都拿着左筷子等右筷子 -> 死锁
    cout << "p" << i + 1 << " picked up RIGHT fork.\n";

    // 3. 开始吃饭
    cout << "p" << i + 1 << ": " << getpid() << " eating\n";
    sleep(rate);
}

注意:上面的代码完全去掉了锁。在真实代码中,你需要把 lock->close_lock() 和 open_lock() 的位置调整,或者让每个筷子成为一个独立的信号量(初值为1),然后每个进程依次 P(左筷子) -> P(右筷子)。如果所有进程同时执行 P(左筷子),就会死锁。

死锁现象:可以看到,所有的哲学家都等着右边的筷子,已经发生死锁了

2. 制造饥饿

原理:饥饿通常发生在资源分配策略不公平时。在哲学家问题中,如果采用“只要左右没人吃我就吃”的策略(即上述代码),虽然避免了死锁,但在极端调度下,某个哲学家可能永远无法获得进餐机会。

更明显的饥饿制造法(3人变2人策略)

假设我们修改逻辑,让哲学家 0 和 1 极其频繁地交替进餐,导致哲学家 2 永远抢不到资源。

修改方法

修改dp.h函数,改变进程的优先级或执行频率(虽然 Linux 调度是公平的,但我们可以通过逻辑模拟)。

#ifndef DP_H
#define DP_H

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

// 必须定义这个联合,用于信号量操作
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

class dp {
private:
    int phil_num;       // 哲学家数量
    int rate;           // 吃饭速率
    int semaphores[5];  // 5个筷子的信号量ID

    // 核心逻辑函数
    void pickup(int i);
    void putdown(int i);

    // 信号量操作封装(关键:这里必须声明)
    void wait_sem(int sem_id);
    void signal_sem(int sem_id);

public:
    // 构造函数和析构函数
    dp(int n, int r);
    ~dp();

    // 进程行为
    void think(int i);
    void eat(int i);
    void run();
};

#endif

更符合实验意图的饥饿(基于优先级)

如果修改 Condition::Signal 函数,总是优先唤醒左边的邻居,而哲学家 4 总是被哲学家 0 和 3 “夹击”。

修改 dp.cc 

#include "dp.h"
#include <iostream>
#include <cstdlib>
#include <sys/wait.h>

using namespace std;

// 全局变量:充当“服务员”的信号量,限制最多4人进餐
int room_sem_id;

// --- 构造函数 ---
dp::dp(int n, int r) : phil_num(n), rate(r) {
    // 1. 创建“服务员”信号量,初始值为 4 (N-1),防止死锁
    room_sem_id = semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT);
    semun arg;
    arg.val = 4;
    semctl(room_sem_id, 0, SETVAL, arg);

    // 2. 创建5根筷子的信号量
    for (int i = 0; i < phil_num; i++) {
        semaphores[i] = semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT);
        arg.val = 1; // 初始值为1,表示筷子可用
        semctl(semaphores[i], 0, SETVAL, arg);
    }
}

// --- 析构函数 ---
dp::~dp() {
    // 清理所有信号量
    semctl(room_sem_id, 0, IPC_RMID);
    for (int i = 0; i < phil_num; i++) {
        semctl(semaphores[i], 0, IPC_RMID);
    }
}

// --- 信号量 P 操作 (Wait) ---
void dp::wait_sem(int sem_id) {
    struct sembuf op;
    op.sem_num = 0;
    op.sem_op = -1; // P操作:减1
    op.sem_flg = 0;
    semop(sem_id, &op, 1);
}

// --- 信号量 V 操作 (Signal) ---
void dp::signal_sem(int sem_id) {
    struct sembuf op;
    op.sem_num = 0;
    op.sem_op = 1;  // V操作:加1
    op.sem_flg = 0;
    semop(sem_id, &op, 1);
}

// --- 拿筷子 ---
void dp::pickup(int i) {
    // 1. 请求进入餐厅 (服务员控制)
    wait_sem(room_sem_id);

    // 2. 拿左边筷子
    wait_sem(semaphores[i]);
    cout << "p" << i + 1 << " picked up LEFT fork " << i << endl;

    // 3. 拿右边筷子
    int right = (i + 1) % phil_num;
    wait_sem(semaphores[right]);
    cout << "p" << i + 1 << " picked up RIGHT fork " << right << endl;
}

// --- 放筷子 ---
void dp::putdown(int i) {
    int right = (i + 1) % phil_num;

    // 1. 放下右边
    signal_sem(semaphores[right]);
    // 2. 放下左边
    signal_sem(semaphores[i]);

    // 3. 离开餐厅
    signal_sem(room_sem_id);

    cout << "p" << i + 1 << " put down forks." << endl;
}

// --- 思考 ---
void dp::think(int i) {
    cout << "p" << i + 1 << " is THINKING..." << endl;
    sleep(1 + rand() % 3);
}

// --- 吃饭 ---
void dp::eat(int i) {
    cout << "p" << i + 1 << " is EATING..." << endl;
    sleep(rate);
}

// --- 制造饥饿的核心逻辑 ---
void dp::run() {
    for (int i = 0; i < phil_num; i++) {
        if (fork() == 0) { // 子进程
            srand(getpid()); // 随机种子
            int id = i;      // 保存ID

            while (true) {
                think(id);

                // --- 制造饥饿的策略 ---
                // 如果是奇数号哲学家 (p2, p4),随机延迟一下再抢
                // 这样偶数号 (p1, p3, p5) 就会更频繁地抢到资源
                if (id % 2 != 0) {
                    sleep(rand() % 2);
                }

                pickup(id);
                eat(id);
                putdown(id);
            }
        }
    }

    // 父进程等待 (按 Ctrl+C 停止)
    while (wait(NULL) > 0);
}

// --- 主函数 ---
int main() {
    dp dining(5, 2); // 5个人,吃2秒
    dining.run();
    return 0;
}

饥饿现象:观察输出,你会发现 p1p2p3p5 都在轮流 eating,但 p4 的输出始终停留在 hungry,永远无法变成 eating

总结

  • 死锁:破坏了“请求与保持”条件或“循环等待”条件(例如所有哲学家同时拿起左手筷子)。
  • 饥饿:破坏了“有限等待”条件(例如调度算法总是偏向某些进程,导致其他进程永远得不到资源)。

独立实验

第一步:

将以下代码完整复制到 railway.cpp 中并保存退出

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>

using namespace std;

// 定义方向枚举
enum Direction { NORTH, SOUTH };

// --- 管程 (Monitor) 类 ---
class RailwayMonitor {
private:
    pthread_mutex_t mutex;          // 互斥锁,保护共享变量
    pthread_cond_t cond;            // 条件变量,用于火车等待和唤醒
    int current_direction;          // 当前允许通行的方向 (-1:空闲, 0:北行, 1:南行)
    int train_count;                // 当前铁路上同方向火车的数量
    int north_waiting;              // 北面等待的火车数量
    int south_waiting;              // 南面等待的火车数量

public:
    RailwayMonitor() {
        pthread_mutex_init(&mutex, NULL);
        pthread_cond_init(&cond, NULL);
        current_direction = -1; // 初始为空闲
        train_count = 0;
        north_waiting = 0;
        south_waiting = 0;
    }

    // 火车申请进入铁路
    void enter(int direction) {
        pthread_mutex_lock(&mutex);

        // 登记等待
        if (direction == NORTH) north_waiting++;
        else south_waiting++;

        // 循环检查等待条件:
        // 1. 铁路上有车且方向不一致(撞车风险)
        // 2. 铁路上没车,但对面有车在等待(公平性/防饥饿策略)
        while (train_count > 0 && current_direction != direction) {
            pthread_cond_wait(&cond, &mutex);
        }
        
        // 如果铁路上没车了,且对面有人在等,强制切换方向(防饥饿核心逻辑)
        if (train_count == 0 && current_direction != -1 && current_direction != direction) {
             // 此时对面会先被唤醒,自己继续等待
             pthread_cond_wait(&cond, &mutex);
        }

        // 成功获得通行权,更新状态
        if (direction == NORTH) north_waiting--;
        else south_waiting--;

        current_direction = direction;
        train_count++;

        // 唤醒其他同方向的火车(允许同方向连续通过,提高效率)
        pthread_cond_broadcast(&cond);

        pthread_mutex_unlock(&mutex);
    }

    // 火车离开铁路
    void exit(int direction) {
        pthread_mutex_lock(&mutex);

        train_count--;

        // 如果铁路上没车了,需要决定下一个方向
        if (train_count == 0) {
            // 防饥饿策略:优先唤醒对面方向的等待者
            if ((direction == NORTH && south_waiting > 0) || 
                (direction == SOUTH && north_waiting > 0)) {
                // 切换方向
                current_direction = (direction == NORTH) ? SOUTH : NORTH;
            } else {
                // 对面没人,继续保持当前方向或设为空闲
                if (north_waiting == 0 && south_waiting == 0) {
                    current_direction = -1;
                }
                // 如果同方向还有人,current_direction 保持不变
            }
            // 唤醒所有等待的火车重新竞争
            pthread_cond_broadcast(&cond);
        }

        pthread_mutex_unlock(&mutex);
    }
};

// 全局管程实例
RailwayMonitor track;

// 火车线程的通用函数
void* train_routine(void* arg) {
    int id = *(int*)arg;
    Direction dir = (id % 2 == 0) ? NORTH : SOUTH; // 偶数ID北行,奇数ID南行
    const char* dir_str = (dir == NORTH) ? "北行" : "南行";

    cout << "火车 " << id << " (" << dir_str << ") 正在等待进入铁路..." << endl;
    
    // 申请进入
    track.enter(dir);
    
    cout << "火车 " << id << " (" << dir_str << ") 正在铁路上行驶..." << endl;
    sleep(2); // 模拟过铁路的时间
    
    cout << "火车 " << id << " (" << dir_str << ") 已经离开铁路。" << endl;
    
    // 申请离开
    track.exit(dir);

    return NULL;
}

int main() {
    const int NUM_TRAINS = 10; // 总共模拟 10 辆火车
    pthread_t trains[NUM_TRAINS];
    int train_ids[NUM_TRAINS];

    cout << "=== 铁路调度模拟开始 (防饥饿策略: 交替通行) ===" << endl;

    // 创建火车线程
    for (int i = 0; i < NUM_TRAINS; i++) {
        train_ids[i] = i + 1;
        // 随机延迟一下创建时间,模拟真实情况
        usleep(rand() % 500000); 
        pthread_create(&trains[i], NULL, train_routine, &train_ids[i]);
    }

    // 等待所有火车通过
    for (int i = 0; i < NUM_TRAINS; i++) {
        pthread_join(trains[i], NULL);
    }

    cout << "=== 所有火车均已安全通过 ===" << endl;
    return 0;
}

第二步:编译代码

在终端中,使用 g++ 编译器进行编译。因为代码使用了 POSIX 线程库,必须加上 -pthread 参数

g++ -o railway railway.cpp -pthread

./railway执行

课后问题

实验总结与分析报告

1. 分析示例实验是否真正模拟了哲学家就餐问题?

回答:
是的,示例实验通常通过多线程来模拟哲学家的行为。每个线程代表一个哲学家,共享资源(筷子/叉子)通过全局变量或信号量表示。哲学家在“思考”(睡眠或循环)和“进餐”(尝试获取左右两边的资源)之间切换。如果代码逻辑正确,它确实模拟了并发环境下的资源竞争、互斥需求以及死锁风险。

2. 为什么示例程序不会产生死锁?

回答:
如果示例程序没有死锁,通常是因为采用了破坏死锁四个必要条件之一的策略。常见的做法包括:

  • 限制人数: 只允许最多4位哲学家同时尝试拿筷子(破坏“循环等待”或“请求与保持”)。
  • 奇偶策略: 奇数号哲学家先拿左手筷子,偶数号先拿右手筷子(破坏“循环等待”)。
  • 原子操作: 使用一个互斥锁保护“同时拿两根筷子”的动作,要么都拿,要么都不拿。
3. 为什么会出现进程死锁和饥饿现象?

回答:

  • 死锁: 当所有哲学家同时拿起左边的筷子,然后都在等待右边的筷子时,就会发生死锁。此时每个进程都持有资源并等待另一个被占用的资源,形成循环等待链。
  • 饥饿: 即使没有死锁,如果调度算法不公平,可能会出现“中间人效应”。例如,哲学家A和C在进餐,夹在中间的哲学家B一直无法同时拿到两根筷子,导致B无限期等待。
4. 怎样利用实验造成和表现死锁和饥饿现象?

回答:

  • 造成死锁: 修改代码,让所有哲学家线程按照相同的顺序(例如都先拿左筷子,pthread_mutex_lock)去获取资源,并且在获取第一根筷子后加入一个明显的延时(sleep),模拟上下文切换,增加死锁发生的概率。
  • 造成饥饿: 设计一个调度策略,让某些高优先级的线程频繁获取资源,或者在释放资源后立即重新竞争成功,导致低优先级线程长期无法获取资源。
5. 管程能避免死锁和饥饿的机理是什么?

回答:

  • 避免死锁: 管程通过互斥锁保证同一时间只有一个线程在执行管程内的代码。配合条件变量,线程在条件不满足时(如路被占用)会释放锁并挂起,而不是忙等待或持有锁等待,从而打破了“请求与保持”条件。
  • 避免饥饿: 管程内部的条件变量通常配合等待队列(FIFO)使用。当资源释放时,按照先进先出的顺序唤醒等待线程,保证了公平性,防止了某个线程长期被忽略。
6. 您对于管程概念有哪些新的理解和认识?

回答:
管程不仅仅是一组函数,它是一种封装了共享变量和操作这些变量的过程的高级同步原语。它将“互斥”和“同步”逻辑封装在一起,使得并发编程更加模块化和安全。相比于直接使用信号量,管程将复杂的同步细节隐藏在模块内部,减少了死锁和竞态条件的风险。

7. 条件变量和信号量有何不同?

回答:

  • 状态保持: 信号量有一个整数值(计数器),signal 操作会增加计数,即使没有线程在等待,这个信号也会保留下来供后续使用。而条件变量没有记忆性,如果在没有线程等待时调用 signal,该信号会丢失。
  • 用途: 信号量既可以用于互斥也可以用于同步。条件变量必须与互斥锁配合使用,专门用于线程间的同步(等待某个条件成立)。
8. 为什么在管程中要使用条件变量而不直接使用信号量?

回答:
因为管程的设计初衷是简化同步。直接使用信号量(P/V操作)容易出错(如顺序颠倒导致死锁)。条件变量提供了更高级的抽象——wait 和 signal,语义更清晰(“等待条件”和“通知条件满足”)。此外,条件变量的 wait 操作会自动释放关联的互斥锁,这是实现管程“互斥进入”特性的关键,用普通信号量很难原子性地实现这一点。

9. 示例实验中构造的管程中的条件变量是一种什么样式的?

回答:
通常有两种样式:Hoare管程Mesa管程

  • Hoare管程: 唤醒者立即挂起,被唤醒者立即运行。
  • Mesa管程(Linux/Java常用): 唤醒者继续执行,被唤醒者进入“入口队列”等待重新获取锁。
    在Linux pthread 中,条件变量属于 Mesa 样式。这意味着在被唤醒后,线程必须在一个 while 循环中再次检查条件是否满足(防止虚假唤醒或条件再次改变)。
10. 其中的锁起了什么样的作用?

回答:
锁(互斥量 mutex)在管程中起到了互斥进入的作用。它保证了在任何时刻,最多只有一个线程在执行管程内部的过程。这是保护共享变量(如单行道中的计数器、方向标志)不被并发修改的基础。

11. 你的独立实验程序是怎样解决单行道问题的?

回答:
我的程序使用了一个管程类 RailwayMonitor

  1. 互斥: 使用 pthread_mutex_t 保护共享状态(当前方向、南北车辆计数)。
  2. 同步逻辑: 定义了 pthread_cond_t 条件变量。
    • 当火车想要进入时,检查当前方向是否允许。如果不允许(例如南行车遇到北行占用),则调用 pthread_cond_wait 进入等待队列。
    • 当火车离开时,减少计数。如果该方向没有车了,就改变允许的方向,并调用 pthread_cond_broadcast 唤醒所有等待的火车。
  3. 避免饥饿: 通过“交替通行”策略(当一边没车了就强制切换方向),确保了双向交通的公平性。
12. 您是怎样构造管程对象的?

回答:
我使用了 C++ 的 class 来封装管程:

  • 私有成员: 放置共享变量(current_directioncount 等)、互斥锁 mutex 和条件变量 cond。这些对外不可见,保证了数据封装。
  • 公共成员函数: 提供 enter_north()leave_north()enter_south()leave_south() 等接口。这些函数内部包含了加锁、检查条件、等待/唤醒、解锁的完整逻辑。
  • 初始化: 在构造函数中初始化锁和条件变量,确保对象创建时处于一致状态。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐