一次讲清多线程并发模型:生产者-消费者、哲学家用餐、读者-作家模型
在多线程里,我们追求的是效率与并行,但随之而来的是对共享资源访问的失控风险。这里深入探讨三个经典的同步问题。
生产者-消费者模型
核心概念与问题描述
生产者-消费者模型是解决线程间解耦和流量控制的经典模式。它描述了一个或多个生产者线程生成数据,并将其放入一个固定大小的缓冲区中;同时,一个或多个消费者线程从缓冲区中取出数据并进行处理。

活动行为
- 缓冲区满时:生产者必须等待,直到消费者取走数据腾出空间。
- 缓冲区空时:消费者必须等待,直到生产者放入新数据。
- 互斥访问:任何时刻,只能有一个线程(生产者或消费者)访问缓冲区,以防止数据不一致。
伪代码实现
我们使用三个信号量来解决问题:
mutex:互斥信号量,初始值为1,用于保护对缓冲区的互斥访问。empty:同步信号量,初始值为缓冲区大小N,表示当前空闲槽位的数量。full:同步信号量,初始值为0,表示当前已填充的产品数量。
// 全局变量定义
#define N 100 // 缓冲区大小
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
item buffer[N];
int in = 0; // 生产者放入位置的指针
int out = 0; // 消费者取出位置的指针
// 生产者进程
void producer() {
while (true) {
item next_produced = produce_item(); // 生产一个数据项
P(empty); // 1. 等待有空闲槽位
P(mutex); // 2. 获取缓冲区访问锁
// --- 临界区开始 ---
buffer[in] = next_produced;
in = (in + 1) % N; // 循环缓冲区指针
// --- 临界区结束 ---
V(mutex); // 3. 释放缓冲区访问锁
V(full); // 4. 通知消费者,产品数量+1
}
}
// 消费者进程
void consumer() {
while (true) {
P(full); // 1. 等待有产品可用
P(mutex); // 2. 获取缓冲区访问锁
// --- 临界区开始 ---
item next_consumed = buffer[out];
out = (out + 1) % N; // 循环缓冲区指针
// --- 临界区结束 ---
V(mutex); // 3. 释放缓冲区访问锁
V(empty); // 4. 通知生产者,空闲槽位+1
consume_item(next_consumed); // 消费数据项
}
}
哲学家就餐问题
核心概念与问题描述
哲学家就餐问题是演示死锁和资源竞争的经典模型。五位哲学家围坐在一张圆桌旁,每人面前有一盘面条,左右两边各有一根筷子(或叉子)。哲学家交替进行思考和就餐。要就餐时,哲学家必须同时拿起左右两边的筷子。就餐完毕后,将两根筷子放回桌上。
问题的核心是设计一个协议,使得哲学家们能够并发地就餐,同时避免以下情况:
- 死锁:如果所有哲学家同时拿起左边的筷子,那么他们将永远等待右边的筷子,导致所有人都无法就餐。
- 饥饿:某些哲学家可能永远没有机会拿到两根筷子。

伪代码实现(一种避免死锁的方案)
一个简单有效的方案是资源分级。我们将五根筷子编号为0到4。规定奇数号哲学家先拿编号小的筷子(左边),偶数号哲学家先拿编号大的筷子(右边)。这打破了循环等待的条件,从而避免了死锁。
// 全局变量定义
semaphore chopstick[5] = {1, 1, 1, 1, 1}; // 每根筷子是一个互斥信号量
// 哲学家进程 (i 为哲学家编号,0-4)
void philosopher(int i) {
while (true) {
think(); // 思考
int first = min(i, (i + 1) % 5); // 先拿编号小的筷子
int second = max(i, (i + 1) % 5); // 后拿编号大的筷子
P(chopstick[first]); // 拿起第一根筷子
P(chopstick[second]); // 拿起第二根筷子
eat(); // 就餐
V(chopstick[second]); // 放下第二根筷子
V(chopstick[first]); // 放下第一根筷子
}
}
读者-作家问题
核心概念与问题描述
读者-作家问题模拟了对共享数据(如文件、数据库)的访问。有两类线程:
- 读者:只读取数据,不修改。多个读者可以同时读取。
- 作家:修改数据。作家需要独占访问,即作家工作时,不允许任何读者或其他作家访问。
该模型的核心在于平衡并发性和数据一致性。读者之间是并发的,但读者和作家、作家和作家之间是互斥的。

伪代码实现(读者优先方案)
“读者优先”意味着只要有读者在读,新来的读者就可以直接进入,这可能导致作家饥饿。我们使用一个计数器 read_count 来记录当前读者的数量。
// 全局变量定义
semaphore rw_mutex = 1; // 作家和第一个/最后一个读者之间的互斥锁
semaphore count_mutex = 1; // 保护 read_count 的互斥锁
int read_count = 0; // 当前读者数量
// 读者进程
void reader() {
while (true) {
// --- 进入区 ---
P(count_mutex);
read_count++;
if (read_count == 1) { // 如果是第一个读者
P(rw_mutex); // 阻止作家进入
}
V(count_mutex);
// --- 临界区:执行读操作 ---
read_data();
// --- 退出区 ---
P(count_mutex);
read_count--;
if (read_count == 0) { // 如果是最后一个读者
V(rw_mutex); // 允许作家进入
}
V(count_mutex);
}
}
// 作家进程
void writer() {
while (true) {
P(rw_mutex); // 获取独占访问权,阻止所有读者和其他作家
// --- 临界区:执行写操作 ---
write_data();
V(rw_mutex); // 释放独占访问权
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)