【OS】Process Management(2)

在这里插入图片描述

《计算机操作系统(第三版)》(汤小丹)学习笔记


4、经典进程的同步问题

eg:

  • 生产者—消费者问题
  • 哲学家进餐问题
  • 读者—写者问题

4.1、生产者—消费者问题(The proceducer-consumer proble)

一、问题定义

生产者—消费者问题是一个经典的同步问题,描述了一组生产者线程和消费者线程共享一个有限大小的缓冲区(或队列)的场景。

生产者负责生成数据并将其放入缓冲区。

消费者负责从缓冲区中取出数据并处理。

目标:确保生产者和消费者能够高效、安全地协作,避免竞态条件和资源浪费。

二、核心挑战

同步问题

  • 生产者不能在缓冲区满时继续放入数据。
  • 消费者不能在缓冲区空时尝试取出数据。

互斥问题

  • 多个生产者和消费者不能同时访问缓冲区,需确保数据一致性。

忙等待问题

  • 避免线程在无数据时频繁轮询缓冲区,浪费CPU资源。

4.1.1、利用记录型信号量解决生产者—消费者问题

以线程为例

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

#define BUFFER_SIZE 10  // 缓冲区大小

int buffer[BUFFER_SIZE];  // 缓冲区数组
int in = 0;               // 生产者放入数据的位置
int out = 0;              // 消费者取出数据的位置

sem_t empty;              // 记录空槽位数的信号量
sem_t full;               // 记录已占用槽位数的信号量
pthread_mutex_t mutex;    // 互斥锁,保护缓冲区的访问

// 生产者函数
void* producer(void* arg) {
    
    
    int item;
    for (int i = 0; i < 20; i++) {
    
    
        item = i;  // 生产数据
        sem_wait(&empty);  // 等待空槽位, empty -1
        pthread_mutex_lock(&mutex);  // 加锁,保护缓冲区

        // 将数据放入缓冲区
        buffer[in] = item;
        printf("Produced: %d at position %d\n", item, in);
        in = (in + 1) % BUFFER_SIZE;  // 更新生产者位置

        pthread_mutex_unlock(&mutex);  // 解锁
        sem_post(&full);  // 增加已占用槽位数, full + 1
    }
    return NULL;
}

// 消费者函数
void* consumer(void* arg) {
    
    
    int item;
    for (int i = 0; i < 20; i++) {
    
    
        sem_wait(&full);  // 等待已占用槽位, full -1
        pthread_mutex_lock(&mutex);  // 加锁,保护缓冲区

        // 从缓冲区取出数据
        item = buffer[out];
        printf("Consumed: %d from position %d\n", item, out);
        out = (out + 1) % BUFFER_SIZE;  // 更新消费者位置

        pthread_mutex_unlock(&mutex);  // 解锁
        sem_post(&empty);  // 增加空槽位数 empty + 1
    }
    return NULL;
}

int main() {
    
    
    pthread_t prod, cons;

    printf("=====Step1=====\n");
    // 初始化信号量和互斥锁
    sem_init(&empty, 0, BUFFER_SIZE);  // 初始空槽位数为缓冲区大小
    sem_init(&full, 0, 0);             // 初始已占用槽位数为0
    pthread_mutex_init(&mutex, NULL);

    printf("=====Step2=====\n");
    // 创建生产者和消费者线程
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);

    printf("=====Step3=====\n");
    // 等待线程结束
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    printf("=====Step4=====\n");
    // 销毁信号量和互斥锁
    sem_destroy(&empty);
    sem_destroy(&full);
    pthread_mutex_destroy(&mutex);

    return 0;
}

output

=====Step1=====
=====Step2=====
=====Step3=====
Produced: 0 at position 0
Produced: 1 at position 1
Produced: 2 at position 2
Produced: 3 at position 3
Produced: 4 at position 4
Produced: 5 at position 5
Produced: 6 at position 6
Produced: 7 at position 7
Produced: 8 at position 8
Produced: 9 at position 9
Consumed: 0 from position 0
Consumed: 1 from position 1
Consumed: 2 from position 2
Consumed: 3 from position 3
Consumed: 4 from position 4
Consumed: 5 from position 5
Consumed: 6 from position 6
Consumed: 7 from position 7
Consumed: 8 from position 8
Consumed: 9 from position 9
Produced: 10 at position 0
Produced: 11 at position 1
Produced: 12 at position 2
Produced: 13 at position 3
Produced: 14 at position 4
Produced: 15 at position 5
Produced: 16 at position 6
Produced: 17 at position 7
Produced: 18 at position 8
Produced: 19 at position 9
Consumed: 10 from position 0
Consumed: 11 from position 1
Consumed: 12 from position 2
Consumed: 13 from position 3
Consumed: 14 from position 4
Consumed: 15 from position 5
Consumed: 16 from position 6
Consumed: 17 from position 7
Consumed: 18 from position 8
Consumed: 19 from position 9
=====Step4=====

注意 wait(mutex) 和 signal(mutex) 需成对出现

wait(empty) 和 signal(empty),wait(full) 和 empty(full) 同理

4.1.2、利用 AND 信号解决生产者—消费者问题

对于生产者消费者问题,也可以利用 AND 信号量来解决,即用 Swait(empty, mutex) 来代替 wait(empty) 和 wait(mutex),用 Ssignal(mutex, full) 来代替 signal(mutex) 和 signal(full),用 Swati(full, mutex) 来代替 wait(full) 和 wait(mutex),用 Ssignal(mutex, empty) 来代替 signal(mutex) 和 signal(empty)

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

#define BUFFER_SIZE 10  // 缓冲区大小

int buffer[BUFFER_SIZE];  // 缓冲区数组
int in = 0;               // 生产者放入数据的位置
int out = 0;              // 消费者取出数据的位置

sem_t empty;              // 记录空槽位数的信号量
sem_t full;               // 记录已占用槽位数的信号量
sem_t mutex;              // 用于互斥访问缓冲区的信号量(模拟互斥锁)

// AND 信号量操作的辅助函数(通过同时操作多个信号量实现)
void sem_wait_and(sem_t *s1, sem_t *s2) {
    
    
    sem_wait(s1);
    sem_wait(s2);
}

void sem_post_and(sem_t *s1, sem_t *s2) {
    
    
    sem_post(s1);
    sem_post(s2);
}

// 生产者函数
void* producer(void* arg) {
    
    
    int item;
    for (int i = 0; i < 20; i++) {
    
    
        item = i;  // 生产数据

        // 使用 AND 信号量操作:同时等待空槽位和互斥锁
        sem_wait_and(&empty, &mutex);

        // 将数据放入缓冲区
        buffer[in] = item;
        printf("Produced: %d at position %d\n", item, in);
        in = (in + 1) % BUFFER_SIZE;  // 更新生产者位置

        // 释放互斥锁和增加已占用槽位数
        sem_post_and(&mutex, &full);
    }
    return NULL;
}

// 消费者函数
void* consumer(void* arg) {
    
    
    int item;
    for (int i = 0; i < 20; i++) {
    
    
        // 使用 AND 信号量操作:同时等待已占用槽位和互斥锁
        sem_wait_and(&full, &mutex);

        // 从缓冲区取出数据
        item = buffer[out];
        printf("Consumed: %d from position %d\n", item, out);
        out = (out + 1) % BUFFER_SIZE;  // 更新消费者位置

        // 释放互斥锁和增加空槽位数
        sem_post_and(&mutex, &empty);
    }
    return NULL;
}

int main() {
    
    
    pthread_t prod, cons;

    // 初始化信号量
    sem_init(&empty, 0, BUFFER_SIZE);  // 初始空槽位数为缓冲区大小
    sem_init(&full, 0, 0);             // 初始已占用槽位数为0
    sem_init(&mutex, 0, 1);            // 初始互斥锁可用

    // 创建生产者和消费者线程
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);

    // 等待线程结束
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    // 销毁信号量
    sem_destroy(&empty);
    sem_destroy(&full);
    sem_destroy(&mutex);

    return 0;
}

上述代码中

sem_wait_and(&empty, &mutex):生产者等待空槽位和互斥锁同时可用。
sem_post_and(&mutex, &full):生产者释放互斥锁并增加已占用槽位数。
sem_wait_and(&full, &mutex):消费者等待已占用槽位和互斥锁同时可用。
sem_post_and(&mutex, &empty):消费者释放互斥锁并增加空槽位数。

output

Produced: 0 at position 0
Produced: 1 at position 1
Consumed: 0 from position 0
Produced: 2 at position 2
Consumed: 1 from position 1
Produced: 3 at position 3
Consumed: 2 from position 2
Produced: 4 at position 4
Consumed: 3 from position 3
Produced: 5 at position 5
Consumed: 4 from position 4
Produced: 6 at position 6
Consumed: 5 from position 5
Produced: 7 at position 7
Consumed: 6 from position 6
Produced: 8 at position 8
Consumed: 7 from position 7
Produced: 9 at position 9
Consumed: 8 from position 8
Produced: 10 at position 0
Consumed: 9 from position 9
Produced: 11 at position 1
Consumed: 10 from position 0
Produced: 12 at position 2
Consumed: 11 from position 1
Produced: 13 at position 3
Consumed: 12 from position 2
Produced: 14 at position 4
Consumed: 13 from position 3
Produced: 15 at position 5
Consumed: 14 from position 4
Produced: 16 at position 6
Consumed: 15 from position 5
Produced: 17 at position 7
Consumed: 16 from position 6
Produced: 18 at position 8
Consumed: 17 from position 7
Produced: 19 at position 9
Consumed: 18 from position 8
Consumed: 19 from position 9

4.1.3、利用管程解决生产者—消费者问题

通过管程机制,可以有效地解决生产者—消费者问题,将共享数据和对共享数据的操作封装在一起,简化了并发编程。管程内部使用互斥锁和条件变量实现线程间的同步,确保了线程安全和资源的高效利用。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
 
#define BUFFER_SIZE 10  // 缓冲区大小
 
// 缓冲区结构体
typedef struct {
    
    
    int buffer[BUFFER_SIZE];  // 缓冲区数组
    int count;                // 当前缓冲区中的元素数量
    int in;                   // 生产者放入数据的位置
    int out;                  // 消费者取出数据的位置
 
    pthread_mutex_t mutex;    // 互斥锁,保护缓冲区
    pthread_cond_t not_full;  // 条件变量:缓冲区不满
    pthread_cond_t not_empty; // 条件变量:缓冲区不空
} Buffer;
 
// 初始化缓冲区
void init_buffer(Buffer* b) {
    
    
    b->count = 0;
    b->in = 0;
    b->out = 0;
    pthread_mutex_init(&b->mutex, NULL);
    pthread_cond_init(&b->not_full, NULL);
    pthread_cond_init(&b->not_empty, NULL);
}
 
// 销毁缓冲区
void destroy_buffer(Buffer* b) {
    
    
    pthread_mutex_destroy(&b->mutex);
    pthread_cond_destroy(&b->not_full);
    pthread_cond_destroy(&b->not_empty);
}
 
// 生产者函数
void* producer(void* arg) {
    
    
    Buffer* b = (Buffer*)arg;
    for (int i = 0; i < 20; i++) {
    
    
        pthread_mutex_lock(&b->mutex);  // 加锁
 
        // 如果缓冲区满,等待
        while (b->count == BUFFER_SIZE) {
    
    
            pthread_cond_wait(&b->not_full, &b->mutex);
        }
 
        // 生产数据并放入缓冲区
        b->buffer[b->in] = i;
        printf("Produced: %d at position %d\n", i, b->in);
        b->in = (b->in + 1) % BUFFER_SIZE;
        b->count++;
 
        // 通知消费者缓冲区不空
        pthread_cond_signal(&b->not_empty);
        pthread_mutex_unlock(&b->mutex);  // 解锁
    }
    return NULL;
}
 
// 消费者函数
void* consumer(void* arg) {
    
    
    Buffer* b = (Buffer*)arg;
    for (int i = 0; i < 20; i++) {
    
    
        pthread_mutex_lock(&b->mutex);  // 加锁
 
        // 如果缓冲区空,等待
        while (b->count == 0) {
    
    
            pthread_cond_wait(&b->not_empty, &b->mutex);
        }
 
        // 从缓冲区取出数据
        int item = b->buffer[b->out];
        printf("Consumed: %d from position %d\n", item, b->out);
        b->out = (b->out + 1) % BUFFER_SIZE;
        b->count--;
 
        // 通知生产者缓冲区不满
        pthread_cond_signal(&b->not_full);
        pthread_mutex_unlock(&b->mutex);  // 解锁
    }
    return NULL;
}
 
int main() {
    
    
    pthread_t prod, cons;
    Buffer buffer;
 
    init_buffer(&buffer);  // 初始化缓冲区
 
    // 创建生产者和消费者线程
    pthread_create(&prod, NULL, producer, &buffer);
    pthread_create(&cons, NULL, consumer, &buffer);
 
    // 等待线程结束
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
 
    destroy_buffer(&buffer);  // 销毁缓冲区
 
    return 0;
}

buffer:存储数据的数组。
count:当前缓冲区中的元素数量。
inout:生产者和消费者操作的位置指针。
mutex:互斥锁,保护缓冲区的访问。
not_fullnot_empty:条件变量,用于线程间的同步。

  • not_full:当缓冲区不满时,通知生产者可以继续生产。
  • not_empty:当缓冲区不空时,通知消费者可以继续消费。

output

Produced: 0 at position 0
Produced: 1 at position 1
Produced: 2 at position 2
Produced: 3 at position 3
Produced: 4 at position 4
Produced: 5 at position 5
Produced: 6 at position 6
Produced: 7 at position 7
Produced: 8 at position 8
Produced: 9 at position 9
Consumed: 0 from position 0
Consumed: 1 from position 1
Consumed: 2 from position 2
Consumed: 3 from position 3
Consumed: 4 from position 4
Consumed: 5 from position 5
Consumed: 6 from position 6
Consumed: 7 from position 7
Consumed: 8 from position 8
Consumed: 9 from position 9
Produced: 10 at position 0
Produced: 11 at position 1
Produced: 12 at position 2
Produced: 13 at position 3
Produced: 14 at position 4
Produced: 15 at position 5
Produced: 16 at position 6
Produced: 17 at position 7
Produced: 18 at position 8
Produced: 19 at position 9
Consumed: 10 from position 0
Consumed: 11 from position 1
Consumed: 12 from position 2
Consumed: 13 from position 3
Consumed: 14 from position 4
Consumed: 15 from position 5
Consumed: 16 from position 6
Consumed: 17 from position 7
Consumed: 18 from position 8
Consumed: 19 from position 9

4.2、哲学家进餐问题(The Dinning Philosophers Problem)

由 Dijksstra 提出并解决的哲学家进餐问题(The Dinning Philosophers Problem)是经典的同步问题。

五个哲学家公用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐,平时一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才进餐。进餐完毕,放下筷子进行思考。

4.2.1、利用记录型信号量解决哲学家进餐问题

信号量用于控制对共享资源(叉子)的访问,确保每次只有一个哲学家能够拿起一根叉子。

一个信号量表示一个叉子,五个信号量构成信号量数组

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdbool.h>

#define NUM_PHILOSOPHERS 5  // 哲学家的数量
#define MAX_EATS 3          // 每个哲学家最大进餐次数

sem_t forks[NUM_PHILOSOPHERS];  // 信号量数组,每个信号量代表一根叉子
int eat_count[NUM_PHILOSOPHERS] = {
    
    0};  // 每个哲学家的进餐次数
bool stop_flag = false;  // 停止标志

// 哲学家线程函数
void* philosopher(void* arg) {
    
    
    int id = *(int*)arg;  // 哲学家的ID

    while (!stop_flag || eat_count[id] < MAX_EATS) {
    
      // 检查停止标志和进餐次数
        // 哲学家思考
        printf("Philosopher %d is thinking.\n", id);
        sleep(rand() % 3);  // 模拟思考时间

        // 检查是否达到最大进餐次数
        if (eat_count[id] >= MAX_EATS) {
    
    
            break;  // 如果达到最大进餐次数,退出循环
        }

        // 哲学家尝试拿起叉子
        printf("Philosopher %d is trying to pick up forks.\n", id);
        
        // 使用记录型信号量:先拿起左边的叉子,再拿起右边的叉子
        sem_wait(&forks[id]);           // 拿起左边的叉子
        sem_wait(&forks[(id + 1) % NUM_PHILOSOPHERS]);  // 拿起右边的叉子

        // 哲学家进餐
        printf("Philosopher %d is eating.\n", id);
        sleep(rand() % 2);  // 模拟进餐时间
        eat_count[id]++;  // 增加进餐次数

        // 哲学家放下叉子
        sem_post(&forks[(id + 1) % NUM_PHILOSOPHERS]);  // 放下右边的叉子
        sem_post(&forks[id]);           // 放下左边的叉子

        printf("Philosopher %d has finished eating (total: %d times).\n", id, eat_count[id]);

        // 检查是否需要设置停止标志(当所有哲学家都达到最大进餐次数时)
        bool all_finished = true;
        for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
            if (eat_count[i] < MAX_EATS) {
    
    
                all_finished = false;
                break;
            }
        }
        if (all_finished) {
    
    
            stop_flag = true;  // 设置停止标志
        }
    }

    printf("Philosopher %d has stopped.\n", id);
    return NULL;
}

int main() {
    
    
    pthread_t philosophers[NUM_PHILOSOPHERS];
    int ids[NUM_PHILOSOPHERS];

    // 初始化信号量(每根叉子初始值为1,表示可用)
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        sem_init(&forks[i], 0, 1);
    }

    // 创建哲学家线程
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        ids[i] = i;
        pthread_create(&philosophers[i], NULL, philosopher, &ids[i]);
    }

    // 等待哲学家线程结束
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        pthread_join(philosophers[i], NULL);
    }

    // 销毁信号量
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        sem_destroy(&forks[i]);
    }

    printf("All philosophers have finished.\n");
    return 0;
}

output

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 0 is trying to pick up forks.
Philosopher 1 is trying to pick up forks.
Philosopher 0 is eating.
Philosopher 2 is trying to pick up forks.
Philosopher 3 is trying to pick up forks.
Philosopher 4 is trying to pick up forks.
Philosopher 2 is eating.
Philosopher 0 has finished eating (total: 1 times).
Philosopher 0 is thinking.
Philosopher 4 is eating.
Philosopher 2 has finished eating (total: 1 times).
Philosopher 2 is thinking.
Philosopher 1 is eating.
Philosopher 0 is trying to pick up forks.
Philosopher 4 has finished eating (total: 1 times).
Philosopher 4 is thinking.
Philosopher 3 is eating.
Philosopher 2 is trying to pick up forks.
Philosopher 1 has finished eating (total: 1 times).
Philosopher 1 is thinking.
Philosopher 0 is eating.
Philosopher 0 has finished eating (total: 2 times).
Philosopher 0 is thinking.
Philosopher 4 is trying to pick up forks.
Philosopher 4 is eating.
Philosopher 4 has finished eating (total: 2 times).
Philosopher 4 is thinking.
Philosopher 2 is eating.
Philosopher 2 has finished eating (total: 2 times).
Philosopher 2 is thinking.
Philosopher 3 has finished eating (total: 1 times).
Philosopher 3 is thinking.
Philosopher 1 is trying to pick up forks.
Philosopher 1 is eating.
Philosopher 1 has finished eating (total: 2 times).
Philosopher 1 is thinking.
Philosopher 0 is trying to pick up forks.
Philosopher 0 is eating.
Philosopher 0 has finished eating (total: 3 times).
Philosopher 0 is thinking.
Philosopher 0 has stopped.
Philosopher 3 is trying to pick up forks.
Philosopher 3 is eating.
Philosopher 3 has finished eating (total: 2 times).
Philosopher 3 is thinking.
Philosopher 4 is trying to pick up forks.
Philosopher 4 is eating.
Philosopher 4 has finished eating (total: 3 times).
Philosopher 4 is thinking.
Philosopher 4 has stopped.
Philosopher 2 is trying to pick up forks.
Philosopher 2 is eating.
Philosopher 2 has finished eating (total: 3 times).
Philosopher 2 is thinking.
Philosopher 2 has stopped.
Philosopher 1 is trying to pick up forks.
Philosopher 1 is eating.
Philosopher 1 has finished eating (total: 3 times).
Philosopher 1 is thinking.
Philosopher 1 has stopped.
Philosopher 3 is trying to pick up forks.
Philosopher 3 is eating.
Philosopher 3 has finished eating (total: 3 times).
Philosopher 3 has stopped.
All philosophers have finished.

4.2.2、利用 AND 信号解决哲学家进餐问题

在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(叉子)后方能进餐,这个本质上就是前面所介绍的 AND 同步问题

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define NUM_PHILOSOPHERS 5
#define MAX_EATS 5

// 哲学家结构体,用于传递参数
typedef struct {
    
    
    int id;
    sem_t* forks;
    int* eat_counts;  // 指向所有哲学家的进餐次数数组
    pthread_mutex_t* count_mutex;  // 用于保护进餐次数更新的互斥锁
    int* stop_flag;  // 指向停止标志
} Philosopher;

// 哲学家思考
void think(int id) {
    
    
    printf("Philosopher %d is thinking.\n", id);
    sleep(rand() % 3 + 1); // 模拟思考时间
}

// 哲学家进餐
void eat(int id) {
    
    
    printf("Philosopher %d is eating.\n", id);
    sleep(rand() % 2 + 1); // 模拟进餐时间
}

// 尝试拿起两个叉子(模拟 AND 信号量)
int try_pick_up_forks(sem_t* forks, int left_fork, int right_fork) {
    
    
    // 由于 POSIX 信号量是阻塞的,我们通过直接尝试获取两个信号量来模拟 AND 信号量
    // 这里假设信号量操作是原子的,并且由于我们按顺序获取,因此不会死锁(在逻辑上模拟 AND)
    sem_wait(&forks[left_fork]);
    sem_wait(&forks[right_fork]);
    return 1; // 成功获取两个叉子
}

// 放下两个叉子
void put_down_forks(sem_t* forks, int left_fork, int right_fork) {
    
    
    sem_post(&forks[right_fork]);
    sem_post(&forks[left_fork]);
}

// 哲学家行为
void* philosopher_action(void* arg) {
    
    
    Philosopher* p = (Philosopher*)arg;
    int id = p->id;
    sem_t* forks = p->forks;
    int* eat_counts = p->eat_counts;
    pthread_mutex_t* count_mutex = p->count_mutex;
    int* stop_flag = p->stop_flag;

    int left_fork = id;
    int right_fork = (id + 1) % NUM_PHILOSOPHERS;

    while (1) {
    
    
        if (*stop_flag) {
    
    
            break;  // 检查停止标志
        }

        think(id);

        // 尝试拿起两个叉子
        try_pick_up_forks(forks, left_fork, right_fork);
        printf("Philosopher %d picked up forks %d and %d.\n", id, left_fork, right_fork);

        eat(id);

        // 更新进餐次数
        pthread_mutex_lock(count_mutex);
        eat_counts[id]++;
        if (eat_counts[id] >= MAX_EATS) {
    
    
            *stop_flag = 1;  // 设置停止标志
        }
        pthread_mutex_unlock(count_mutex);

        // 放下两个叉子
        put_down_forks(forks, left_fork, right_fork);
        printf("Philosopher %d put down forks %d and %d.\n", id, left_fork, right_fork);

        // 检查停止标志(在释放叉子后,避免死锁情况下无法停止)
        if (*stop_flag) {
    
    
            break;
        }
    }
    return NULL;
}

int main() {
    
    
    pthread_t threads[NUM_PHILOSOPHERS];
    sem_t forks[NUM_PHILOSOPHERS];
    int eat_counts[NUM_PHILOSOPHERS] = {
    
    0};
    pthread_mutex_t count_mutex;
    int stop_flag = 0;
    Philosopher philosophers[NUM_PHILOSOPHERS];

    // 初始化信号量和互斥锁
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        sem_init(&forks[i], 0, 1); // 每个叉子初始为1(可用)
    }
    pthread_mutex_init(&count_mutex, NULL);

    // 创建哲学家线程
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        philosophers[i].id = i;
        philosophers[i].forks = forks;
        philosophers[i].eat_counts = eat_counts;
        philosophers[i].count_mutex = &count_mutex;
        philosophers[i].stop_flag = &stop_flag;
        pthread_create(&threads[i], NULL, philosopher_action, &philosophers[i]);
    }

    // 等待所有线程结束
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        pthread_join(threads[i], NULL);
    }

    // 销毁信号量和互斥锁
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
    
    
        sem_destroy(&forks[i]);
    }
    pthread_mutex_destroy(&count_mutex);

    printf("All philosophers have finished eating %d times.\n", MAX_EATS);

    return 0;
}

try_pick_up_forks:尝试拿起两个叉子,这里直接调用 sem_wait 两次来模拟 AND 信号量。由于 sem_wait 是阻塞的,这里假设按顺序获取不会死锁(在实际应用中需要小心处理可能的死锁情况)。

put_down_forks:放下两个叉子,调用 sem_post 两次。

哲学家行为:哲学家在思考和进餐之间循环,使用封装的函数来获取和释放叉子。

同步机制:使用 pthread_mutex_t 来保护对 eat_counts 和 stop_flag 的访问,确保线程安全。

加锁:在更新 eat_counts 之前,先获取互斥锁,确保没有其他线程正在访问或修改 eat_counts。
解锁:在更新操作完成后,释放互斥锁,允许其他线程访问 eat_counts。

停止机制:当某个哲学家的进餐次数达到 MAX_EATS 时,设置 stop_flag,通知所有线程停止运行。

这个实现通过逻辑顺序(先尝试拿起左边的叉子,再尝试拿起右边的叉子)来模拟 AND 信号量的行为。由于 POSIX 信号量是阻塞的,这种实现方式在逻辑上模拟了 AND 信号量的效果。

output

Philosopher 0 is thinking.
Philosopher 1 is thinking.
Philosopher 2 is thinking.
Philosopher 3 is thinking.
Philosopher 4 is thinking.
Philosopher 0 picked up forks 0 and 1.
Philosopher 0 is eating.
Philosopher 2 picked up forks 2 and 3.
Philosopher 2 is eating.
Philosopher 0 put down forks 0 and 1.
Philosopher 0 is thinking.
Philosopher 4 picked up forks 4 and 0.
Philosopher 4 is eating.
Philosopher 1 picked up forks 1 and 2.
Philosopher 1 is eating.
Philosopher 2 put down forks 2 and 3.
Philosopher 2 is thinking.
Philosopher 4 put down forks 4 and 0.
Philosopher 4 is thinking.
Philosopher 3 picked up forks 3 and 4.
Philosopher 3 is eating.
Philosopher 0 picked up forks 0 and 1.
Philosopher 0 is eating.
Philosopher 1 put down forks 1 and 2.
Philosopher 1 is thinking.
Philosopher 0 put down forks 0 and 1.
Philosopher 0 is thinking.
Philosopher 3 put down forks 3 and 4.
Philosopher 3 is thinking.
Philosopher 2 picked up forks 2 and 3.
Philosopher 2 is eating.
Philosopher 4 picked up forks 4 and 0.
Philosopher 4 is eating.
Philosopher 2 put down forks 2 and 3.
Philosopher 2 is thinking.
Philosopher 1 picked up forks 1 and 2.
Philosopher 1 is eating.
Philosopher 4 put down forks 4 and 0.
Philosopher 4 is thinking.
Philosopher 3 picked up forks 3 and 4.
Philosopher 3 is eating.
Philosopher 1 put down forks 1 and 2.
Philosopher 1 is thinking.
Philosopher 0 picked up forks 0 and 1.
Philosopher 0 is eating.
Philosopher 3 put down forks 3 and 4.
Philosopher 3 is thinking.
Philosopher 0 put down forks 0 and 1.
Philosopher 0 is thinking.
Philosopher 2 picked up forks 2 and 3.
Philosopher 2 is eating.
Philosopher 4 picked up forks 4 and 0.
Philosopher 4 is eating.
Philosopher 2 put down forks 2 and 3.
Philosopher 2 is thinking.
Philosopher 1 picked up forks 1 and 2.
Philosopher 1 is eating.
Philosopher 4 put down forks 4 and 0.
Philosopher 4 is thinking.
Philosopher 3 picked up forks 3 and 4.
Philosopher 3 is eating.
Philosopher 1 put down forks 1 and 2.
Philosopher 1 is thinking.
Philosopher 0 picked up forks 0 and 1.
Philosopher 0 is eating.
Philosopher 3 put down forks 3 and 4.
Philosopher 3 is thinking.
Philosopher 2 picked up forks 2 and 3.
Philosopher 2 is eating.
Philosopher 0 put down forks 0 and 1.
Philosopher 0 is thinking.
Philosopher 4 picked up forks 4 and 0.
Philosopher 4 is eating.
Philosopher 2 put down forks 2 and 3.
Philosopher 2 is thinking.
Philosopher 1 picked up forks 1 and 2.
Philosopher 1 is eating.
Philosopher 4 put down forks 4 and 0.
Philosopher 4 is thinking.
Philosopher 3 picked up forks 3 and 4.
Philosopher 3 is eating.
Philosopher 1 put down forks 1 and 2.
Philosopher 1 is thinking.
Philosopher 0 picked up forks 0 and 1.
Philosopher 0 is eating.
Philosopher 3 put down forks 3 and 4.
Philosopher 3 is thinking.
Philosopher 2 picked up forks 2 and 3.
Philosopher 2 is eating.
Philosopher 0 put down forks 0 and 1.
Philosopher 4 picked up forks 4 and 0.
Philosopher 4 is eating.
Philosopher 2 put down forks 2 and 3.
Philosopher 1 picked up forks 1 and 2.
Philosopher 1 is eating.
Philosopher 4 put down forks 4 and 0.
Philosopher 3 picked up forks 3 and 4.
Philosopher 3 is eating.
Philosopher 1 put down forks 1 and 2.
Philosopher 3 put down forks 3 and 4.
All philosophers have finished eating 5 times.

4.3、读者—写者问题

允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱

不允许一个 Writer 进程和其它 Reader 进程或 Writer 进程同时访问共享对象。

4.3.1、利用记录型信号解决读者—写者问题

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 定义信号量
sem_t wmutex;     // 控制写者互斥访问
sem_t rmutex;     // 保护读者计数器read_count的互斥访问

int read_count = 0; // 当前活跃的读者数量
volatile int terminate_flag = 0; // 终止标志(volatile保证可见性)

void* reader(void* arg) {
    
    
    int id = *(int*)arg;
    
    while(1) {
    
    
        // 检查终止标志
        if(terminate_flag) {
    
    
            printf("读者 %d 收到终止信号,正在退出...\n", id);
            pthread_exit(NULL);
        }
        
        // 获取读者计数器的互斥访问权
        sem_wait(&rmutex);
        read_count++;

        if(read_count == 1) {
    
    
            // 第一个读者获取写者互斥信号量
            sem_wait(&wmutex);
        }

        sem_post(&rmutex);

        // 执行读操作
        printf("读者 %d 正在读取数据...\n", id);
        _sleep(1);

        // 读者离开时更新计数器
        sem_wait(&rmutex);
        read_count--;

        if(read_count == 0) {
    
    
            // 最后一个读者释放写者互斥信号量
            sem_post(&wmutex);
        }
        sem_post(&rmutex);
    }
    return NULL;
}

void* writer(void* arg) {
    
    
    int id = *(int*)arg;
    
    while(1) {
    
    
        // 检查终止标志
        if(terminate_flag) {
    
    
            printf("写者 %d 收到终止信号,正在退出...\n", id);
            pthread_exit(NULL);
        }
        
        // 获取写者互斥信号量
        sem_wait(&wmutex);
        
        // 执行写操作
        printf("写者 %d 正在写入数据...\n", id);
        _sleep(2);
        
        // 释放写者互斥信号量
        sem_post(&wmutex);
    }
    return NULL;
}

// 信号处理函数(用于优雅终止)
void handle_signal(int sig) {
    
    
    terminate_flag = 1; // 设置终止标志
    printf("\n收到终止信号,正在安全退出...\n");
    
    // 唤醒所有等待的线程(确保他们能检测到终止标志)
    sem_post(&wmutex);
    sem_post(&rmutex);
}

int main() {
    
    
    // 注册信号处理
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);

    // 初始化信号量
    sem_init(&wmutex, 0, 1);
    sem_init(&rmutex, 0, 1);

    pthread_t readers[3], writers[2];
    int ids[] = {
    
    1, 2, 3};

    // 创建读者线程
    for(int i = 0; i < 3; i++) {
    
    
        pthread_create(&readers[i], NULL, reader, &ids[i]);
    }

    // 创建写者线程
    for(int i = 0; i < 2; i++) {
    
    
        pthread_create(&writers[i], NULL, writer, &ids[i]);
    }

    // 等待用户输入(保持主线程存活)
    printf("程序运行中... (按Ctrl+C终止)\n");
    while(getchar() != EOF);

    // 等待所有线程退出
    for(int i = 0; i < 3; i++) {
    
    
        pthread_join(readers[i], NULL);
    }
    for(int i = 0; i < 2; i++) {
    
    
        pthread_join(writers[i], NULL);
    }

    // 销毁信号量
    sem_destroy(&wmutex);
    sem_destroy(&rmutex);

    return 0;
}

信号量定义:

  • wmutex:控制写者互斥访问,初始值为1(保证同时只有一个写者)
  • rmutex:保护读者计数器 read_count 的原子操作,初始值为1

读者逻辑:

  • 第一个读者获取 wmutex 阻止写者
  • 读者计数器 read_count 使用 rmutex 保护原子性
  • 最后一个读者释放 wmutex 允许写者

写者逻辑:

  • 获取 wmutex 后独占访问资源
  • 完成操作后释放信号量

同步策略:

  • 读者优先:当有读者正在读时,后续读者可直接进入
  • 计数器保护:使用独立的互斥信号量保护共享变量

该实现通过POSIX信号量的PV操作,确保了读者-写者问题的三个核心需求:读者并发、读写互斥、写者互斥

output

...
...
...
读者 1 正在读取数据...
读者 3 正在读取数据...
读者 1 正在读取数据...
读者 2 正在读取数据...
读者 3 正在读取数据...
读者 1 正在读取数据...
读者 2 正在读取数据...
写者 1 正在写入数据...
写者 2 正在写入数据...
读者 3 正在读取数据...
读者 1 正在读取数据...
...
...
...

有读者就不让写,没有读者才可以写

4.3.2、利用信号量集机制解决读者—写者问题

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define MAX_READERS 3  // 读者数量控制信号量(初始值为MAX_READERS)

sem_t mx;  // 尝试获取读者数量控制信号量(非阻塞)
sem_t L;  // 读者数量控制信号量(初始值为MAX_READERS)

// 新增辅助函数
int get_reader_count() {
    
    
    int current_value;
    sem_getvalue(&L, &current_value);  // 正确获取当前值
    return MAX_READERS - current_value;
}

void* reader(void* arg) {
    
    
    int id = *(int*)arg;
    
    while(1) {
    
    
        // 尝试获取读者数量控制信号量(非阻塞), 0 表示成功
        if(sem_trywait(&L) == 0) {
    
    
            printf("读者 %d 进入(当前读者数:%d/%d)\n", 
                  id, get_reader_count(), MAX_READERS);

            // 获取互斥信号量(非阻塞方式,保证写者优先)
            if(sem_trywait(&mx) == 0) {
    
    
                printf("读者 %d 正在读取数据...\n", id);
                sleep(1);
                sem_post(&mx);
                sem_post(&L);
                printf("读者 %d 离开(剩余读者数:%d/%d)\n", 
                      id, get_reader_count(), MAX_READERS);
            } else {
    
    
                // 如果获取互斥信号量失败,立即释放读者数量控制信号量
                sem_post(&L);
                printf("读者 %d 等待中(写者优先)...\n", id);
                sleep(1);
            }
        } else {
    
    
            printf("读者 %d 被阻塞(达到最大读者数)...\n", id);
            sleep(1);
        }
    }
    return NULL;
}

void* writer(void* arg) {
    
    
    int id = *(int*)arg;
    
    while(1) {
    
    
        sem_wait(&mx);
        printf("写者 %d 正在写入数据...\n", id);
        sleep(2);
        sem_post(&mx);
    }
    return NULL;
}

int main() {
    
    
    // 初始化信号量
    sem_init(&mx, 0, 1);
    sem_init(&L, 0, MAX_READERS);

    pthread_t readers[5], writers[2];
    int ids[] = {
    
    1, 2, 3, 4, 5};

    // 创建读者线程(超过最大读者数)
    for(int i = 0; i < 5; i++) {
    
    
        pthread_create(&readers[i], NULL, reader, &ids[i]);
    }

    // 创建写者线程
    for(int i = 0; i < 2; i++) {
    
    
        pthread_create(&writers[i], NULL, writer, &ids[i]);
    }

    // 主线程休眠(保持程序运行)
    printf("程序运行中...(按Ctrl+C终止)\n");
    while(1) sleep(1);

    // 清理信号量(实际不会执行到这里)
    sem_destroy(&mx);
    sem_destroy(&L);
    return 0;
}

信号量定义:

  • sem_t mx:互斥信号量,初始值为1,用于保证读写操作的互斥性
  • sem_t L:读者数量控制信号量,初始值为 MAX_READERS,用于限制同时读取的读者数量

读者逻辑:

  • 首先尝试获取L信号量(非阻塞方式)
  • 如果成功获取L,再尝试获取mx信号量(非阻塞方式)
  • 只有同时获取两个信号量时才能执行读操作
  • 读操作完成后先释放mx,再释放L
  • 如果获取信号量失败,会立即释放已获取的信号量并等待

写者逻辑:

  • 直接获取 mx 信号量(阻塞方式)
  • 执行写操作后释放 mx 信号量

读者数量控制:

  • 通过L信号量的初始值MAX_READERS控制最大并发读者数
  • sem_getvalue 可以实时查看剩余可用读者名额

写者优先机制:

  • 读者在获取 mx 信号量时使用非阻塞方式(sem_trywait)
  • 如果写者正在等待 mx 信号量,新到达的读者会立即释放已获取的 L 信号量并等待

output

...
读者 1 进入(当前读者数:1/3)
读者 1 等待中(写者优先)...
读者 3 进入(当前读者数:1/3)
读者 3 等待中(写者优先)...
读者 4 进入(当前读者数:2/3)
读者 4 等待中(写者优先)...
读者 2 进入(当前读者数:1/3)
读者 2 等待中(写者优先)...
读者 5 进入(当前读者数:2/3)
读者 5 等待中(写者优先)...
写者 1 正在写入数据...
读者 1 进入(当前读者数:1/3)
读者 1 等待中(写者优先)...
读者 3 进入(当前读者数:1/3)
读者 3 等待中(写者优先)...
读者 4 进入(当前读者数:2/3)
读者 4 等待中(写者优先)...
读者 5 进入(当前读者数:1/3)
读者 5 等待中(写者优先)...
读者 2 进入(当前读者数:2/3)
读者 2 等待中(写者优先)...
读者 1 进入(当前读者数:1/3)
读者 1 等待中(写者优先)...
读者 2 进入(当前读者数:3/3)
读者 2 等待中(写者优先)...
读者 3 被阻塞(达到最大读者数)...
读者 4 进入(当前读者数:2/3)
读者 4 等待中(写者优先)...
读者 5 被阻塞(达到最大读者数)...
写者 2 正在写入数据...
读者 1 进入(当前读者数:1/3)
读者 1 等待中(写者优先)...
读者 3 进入(当前读者数:1/3...

附录——POSIX 标准库

在 C 语言中,信号量(Semaphore)主要通过 POSIX 标准库 <semaphore.h> 实现

一、核心函数及语法

(1)初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

sem: 信号量对象指针

pshared:

  • 0:线程间共享(同一进程)
  • 非0:进程间共享(需配合共享内存)

value: 信号量初始值

(2)销毁信号量

int sem_destroy(sem_t *sem);

(3)P 操作(等待信号量)

int sem_wait(sem_t *sem);  // 若信号量值≤0则阻塞
int sem_trywait(sem_t *sem); // 非阻塞版,失败立即返回

(4)V 操作(释放信号量)

int sem_post(sem_t *sem);

(5)获取信号量值

int sem_getvalue(sem_t *sem, int *sval);

二、返回值说明

所有函数均返回:

  • 0:成功
  • -1:失败(需检查 errno)

三、关键注意事项

初始化顺序:先初始化信号量再创建使用它的线程

死锁预防:确保 P/V 操作成对出现,避免循环等待

进程间共享:当 pshared 非零时,需配合共享内存使用

信号量类型

  • 二值信号量(0/1):用作互斥锁
  • 计数信号量(>1):管理资源池

性能优化:频繁操作的信号量建议使用更轻量的同步机制(如自旋锁)

四、调试技巧

  • 使用 sem_getvalue() 检查信号量当前值

  • 通过 strace 工具跟踪信号量操作

  • 在关键位置添加日志输出,观察线程执行顺序

信号量是构建复杂并发程序的基础工具,合理设计信号量的初始值和 P/V 操作顺序,可以简洁高效地解决生产者-消费者、读者-写者等经典并发问题。


更多有趣的计算机知识和代码示例,可参考【Programming】