Linux生产消费者模式
生产者消费者问题(Producer-Consumer Problem)是一个经典的多线程同步问题,它展示了线程之间如何共享资源,并通过条件变量来协调它们的执行顺序。
生产者和消费者线程共享一个有限的缓冲区(如队列、栈或其他数据结构)。生产者线程负责将数据放入缓冲区,消费者线程负责从缓冲区取出数据。问题的关键在于:
1.生产者不能在缓冲区已满时再生产;
2.消费者不能在缓冲区为空时去消费。
3.生产者之间为互斥关系,消费者之间也为互斥关系,生产者与消费者之间既为互斥关系也为同步关系。
10.1 基于Blocking Queue的生产者消费者模型
在多线程编程中,阻塞队列(Blocking Queue)是一种特殊的线程安全队列,其主要特点是:(1) 队列为空时,消费者线程试图获取元素会阻塞,直到生产者线程放入新元素。(2) 队列满时,生产者线程试图添加元素会阻塞,直到消费者线程取出元素。
这种特性使得阻塞队列在生产者-消费者模型中非常有用,可以有效地解耦生产者和消费者,避免忙等待,提高系统效率。生产者消费者实际运行逻辑可见下图
示例:
#include <pthread.h> #include <iostream> #include <queue> #include <unistd.h> const int Max_num = 5; std::queue<int> q; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER; pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER; int i = 1; void *produce(void *arg) { while (true) { // 加锁 pthread_mutex_lock(&mutex); // 如果队列满了,阻塞 while (q.size() == Max_num) { pthread_cond_wait(&cond_producer, &mutex); } // 队列没满,生产 q.push(i); std::cout << "生产商品编号:" << i++ << std::endl; // 唤醒消费者线程 pthread_cond_signal(&cond_consumer); // 解锁 pthread_mutex_unlock(&mutex); sleep(1); // 模拟生产耗时 } return NULL; } void *consumer(void *arg) { while (true) { // 加锁 pthread_mutex_lock(&mutex); // 如果队列空了,阻塞 while (q.empty()) { pthread_cond_wait(&cond_consumer, &mutex); } // 队列没空,消费 int item = q.front(); q.pop(); std::cout << "消费商品编号:" << item << std::endl; // 唤醒生产者线程 pthread_cond_signal(&cond_producer); // 解锁 pthread_mutex_unlock(&mutex); sleep(2); } return NULL; } int main() { pthread_t Tid_produced1; pthread_t Tid_produced2; pthread_t Tid_produced3; pthread_t Tid_consumer1; pthread_t Tid_consumer2; pthread_t Tid_consumer3; pthread_create(&Tid_produced1, NULL, produce, NULL); pthread_create(&Tid_consumer1, NULL, consumer, NULL); pthread_create(&Tid_produced2, NULL, produce, NULL); pthread_create(&Tid_consumer2, NULL, consumer, NULL); pthread_create(&Tid_produced3, NULL, produce, NULL); pthread_create(&Tid_consumer3, NULL, consumer, NULL); pthread_join(Tid_produced1, NULL); pthread_join(Tid_consumer1, NULL); pthread_join(Tid_produced2, NULL); pthread_join(Tid_consumer2, NULL); pthread_join(Tid_produced3, NULL); pthread_join(Tid_consumer3, NULL); return 0; } |
运行结果:
生产商品编号:1 消费商品编号:1 生产商品编号:2 消费商品编号:2 生产商品编号:3 消费商品编号:3 生产商品编号:4 生产商品编号:5 生产商品编号:6 消费商品编号:4 消费商品编号:5 消费商品编号:6 生产商品编号:7 生产商品编号:8 生产商品编号:9 …… |
10.2 基于环形队列的生产者消费者模型
10.2.1 POSIX信号量
信号量的本质是一种计数器,用于控制对共享资源的访问,它实现了多线程或多进程间的同步和互斥。信号量通过维护一个整数值(通常称为信号量的值)来决定哪些线程或进程能够进入临界区或访问某些资源。
信号量的组成
计数器:信号量内部维护一个整数值。这个值表示可用的资源数目或临界区的访问权限。当信号量的值大于0时,表示有资源可用,线程或进程可以获取资源或进入临界区。当信号量的值为0时,表示没有资源或无法进入临界区,调用该信号量操作的线程或进程将被阻塞,直到信号量的值大于0。
原子操作:信号量的操作(wait、post)是原子的,即操作在执行时不会被中断。操作的原子性确保了多个线程或进程访问信号量时不会发生竞争条件或数据不一致的问题。
同步机制:信号量通过提供对共享资源的协调访问机制,确保多个线程或进程之间不会发生冲突。
信号量如何工作:
P操作(等待操作):
也叫“等待”或“减值”操作,表示一个线程或进程请求访问共享资源或进入临界区。信号量的值会减少1。如果信号量的值大于0,线程或进程就可以进入临界区。如果信号量的值为0,则表示没有资源或其他线程或进程已经在执行临界区的任务,此时调用sem_wait()的线程或进程会被阻塞,直到信号量值大于0。
V操作(释放操作):
也叫“释放”或“增值”操作,表示一个线程或进程完成了对共享资源的访问或临界区的执行。它会将信号量的值增加1,并唤醒任何因等待信号量而阻塞的线程或进程。通过sem_post()操作,信号量的值增加,并可能通知其他等待的线程或进程它们可以继续执行。
10.2.2 信号量操作
初始化信号量(适用于线程间同步)
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:指向信号量的指针
pshared = 0:表示用于线程间共享(同一进程)
value:信号量初始值,如value=1,则作用近似于互斥锁
返回值:0 表示成功,-1 表示失败
等待信号量(P 操作)
int sem_wait(sem_t *sem); // 阻塞等待
int sem_trywait(sem_t *sem); // 非阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 限时等待
如果 sem 的值大于 0,减 1 后继续执行。
如果为 0,sem_wait() 会阻塞当前线程,直到其他线程释放
释放信号量(V 操作)
int sem_post(sem_t *sem);
增加信号量的值,若有等待线程,则唤醒其中一个
毁信号量
int sem_destroy(sem_t *sem);
10.2.3 信号量示例
#include <semaphore.h> #include <iostream> #include <unistd.h> #include <pthread.h> int num = 100; sem_t sem; // 二元信号量 void *run(void *arg) { while (true) { if (num > 0) { sem_wait(&sem); // P操作 std::cout << "当前num值:" << num-- << std::endl; sleep(1); sem_post(&sem); } else { break; } } return NULL; } int main() { pthread_t tid[3]; sem_init(&sem, 0, 1); pthread_create(&tid[0], NULL, run, NULL); pthread_create(&tid[1], NULL, run, NULL); pthread_create(&tid[2], NULL, run, NULL); pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_join(tid[2], NULL); sem_destroy(&sem); return 0; } |
10.2.4 基于环形队列的生产者消费者模型
环形队列的原理基于一个数组,通过设计使得队列的头部和尾部在一个固定的数组范围内循环,避免了传统队列因空间满而无法继续存储数据的问题。其基本思想就是将数组的末尾与开头连接形成一个环形结构,因此它也称作“循环队列”。
环形队列是通过一个固定大小的数组来实现的。这个数组的两个关键指针是 head 和 tail,分别指向队列的头部和尾部。
队列的“循环”特性体现在这两个指针的移动方式上。当尾指针或头指针达到数组的末尾时,它们会自动回到数组的开头位置,从而形成一个环形结构。
环形队列的操作流程
初始化队列:
设置一个数组作为队列的存储空间,初始化 front 和 rear 为 0,表示队列为空。
入队操作:
在入队时,检查队列是否已满。如果未满,将新元素添加到 head 指针指向的位置,然后更新 head 指针 (head + 1) % max_size。
如果队列已满,则阻止插入,可能通过等待或丢弃元素来处理(具体取决于实现策略)。
出队操作:
在出队时,检查队列是否为空。如果非空,取出 tail 指针指向的元素,并更新 tail 指针 (tail + 1) % max_size。
如果队列为空,则不能执行出队操作,可能通过等待或返回错误来处理。
示例:
Ring_queue.hpp
#include<semaphore.h> #include<vector> #include<pthread.h> #include<iostream> #include<unistd.h> #define NUM 10 class Ring_queue { public: Ring_queue(int cap = NUM) : capacity(cap), pro_pos(0), con_pos(0) { sem_init(&space, 0, capacity); sem_init(&data, 0, 0); pthread_mutex_init(&lock, nullptr); q.resize(capacity); } ~Ring_queue() { sem_destroy(&space); sem_destroy(&data); pthread_mutex_destroy(&lock); } void add(int &arg) { sem_wait(&space); pthread_mutex_lock(&lock); // 加锁 q[pro_pos] = arg; pro_pos = (pro_pos + 1) % capacity; pthread_mutex_unlock(&lock); // 解锁 sem_post(&data); } void take(int &arg) { sem_wait(&data); pthread_mutex_lock(&lock); // 加锁 arg = q[con_pos]; con_pos = (con_pos + 1) % capacity; pthread_mutex_unlock(&lock); // 解锁 sem_post(&space); } private: std::vector<int> q; int capacity; int pro_pos; int con_pos; sem_t space; sem_t data; pthread_mutex_t lock; // 新增互斥锁 }; |
main.cpp
#include "Ring_queue.hpp" void *produce(void *arg) { Ring_queue *q=(Ring_queue *)arg; int num = 0; while (1) { q->add(num); sleep(0.2); std::cout << "生产者发送数据:" << num << std::endl; num++; } return NULL; } void *consume(void *arg) { Ring_queue *q=(Ring_queue *)arg; int num=0; while(1) { q->take(num); sleep(0.5); std::cout << "消费者拿取数据:" << num << std::endl; } return NULL; } int main() { pthread_t tid_pro; pthread_t tid_con; Ring_queue q; pthread_create(&tid_pro,NULL,produce,(void *)&q); pthread_create(&tid_con,NULL,consume,(void *)&q); pthread_join(tid_pro,NULL); pthread_join(tid_con,NULL); return 0; } |
Makefile
all:main main:main.cpp Ring_queue.hpp g++ -o main main.cpp -lpthread -std=c++11 .PHONY:clean clean: rm -f main |