在多线程里,我们追求的是效率与并行,但随之而来的是对共享资源访问的失控风险。这里深入探讨三个经典的同步问题。



生产者-消费者模型

核心概念与问题描述
生产者-消费者模型是解决线程间解耦和流量控制的经典模式。它描述了一个或多个生产者线程生成数据,并将其放入一个固定大小的缓冲区中;同时,一个或多个消费者线程从缓冲区中取出数据并进行处理。

在这里插入图片描述

活动行为

  • 缓冲区满时:生产者必须等待,直到消费者取走数据腾出空间。
  • 缓冲区空时:消费者必须等待,直到生产者放入新数据。
  • 互斥访问:任何时刻,只能有一个线程(生产者或消费者)访问缓冲区,以防止数据不一致。

伪代码实现
我们使用三个信号量来解决问题:

  • 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); // 释放独占访问权
    }
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐