《计算机操作系统(第三版)》(汤小丹)学习笔记
文章目录
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
:当前缓冲区中的元素数量。
in
和 out
:生产者和消费者操作的位置指针。
mutex
:互斥锁,保护缓冲区的访问。
not_full
和 not_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, ¤t_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】