生产者-消费者模型
多线程场景的的典型应用,应用场景非常广泛!手撕!!!
- 消费者和消费者之间,是一个互斥关系。
- 生产者和生产者之间,是一个互斥关系。
- 生产者和消费者之间,是一个互斥同步关系。
注:同步互斥不一定非要用互斥锁和条件变量,还可以用信号量。
代码示例1:(栈实现,互斥锁/条件变量实现互斥同步)
#include <stdio.h> #include<pthread.h> //头文件 #include <unistd.h> //sleep头文件 #include <vector> //定义一个互斥锁 pthread_mutex_t mutex; //实现一个生产者消费者模型 //首先要有一个交易场所 std::vector<int> data; //两个角色:生产者/消费者——两个线程 void* Product(void* arg) { (void*)arg; int count = 0; //负责把数据送到的交易场所中 while (1) { pthread_mutex_lock(&lock); data.push_back(++count); pthread_mutex_unlock(&lock); usleep(789789); } return NULL; } void* Consume(void* arg) { (void*)arg; //负责把交易场所中的数据获取出来 while (1) { pthread_mutex_lock(&lock); //每次取最后一个元素 //注意判空 if (!data.empty()) { int result = data.back(); data.pop_back(); printf("result = %d\n", result); } pthread_mutex_unlock(&lock); usleep(123123); } return NULL; } int main() { pthread_mutex_init(&lock, NULL);//互斥锁初始化函数 pthread_t tid1, tid2; pthread_create(&tid1, NULL, Product, NULL); //创建新线程 pthread_create(&tid2, NULL, Consume, NULL); pthread_join(tid1,NULL); //线程等待 pthread_join(tid2, NULL); pthread_mutex_destory(&lock);//互斥锁释放函数 system("pause"); return 0; } //不加互斥锁: //但是使用过多线程,就有可能有段错误。 //因为C++STL中所提供的容器和算法都是线程不安全[重要] //加上互斥锁:保证线程安全。
结果:
实现了不断加1,打印。但是使用过多线程,就有可能有段错误。因为 C++STL 中所提供的容器和算法都是线程不安全[重要]。加上互斥锁就好了。
刚刚互斥锁这是实现了:消费者和生产者的互斥关系,效率不是很高。消费者做了很多无用功,因为他的速度更快。这时候可以同步进一步改进。如果vector 没数据,消费者就等,有数据才真正进行消费。
下面是加上条件变量——即实现同步。谁快谁等。
完整代码:
#include <stdio.h> #include<pthread.h> //头文件 #include <unistd.h> //sleep头文件 #include <vector> //定义一个互斥锁 pthread_mutex_t mutex; //定义同步——条件变量 pthread_cond_t cond; //实现一个生产者消费者模型 //首先要有一个交易场所 std::vector<int> data; //两个角色:生产者/消费者——两个线程 void* Product(void* arg) { (void*)arg; int count = 0; //负责把数据送到的交易场所中 while (1) { pthread_mutex_lock(&lock); data.push_back(++count); pthread_mutex_unlock(&lock); pthread_cond_signal(&lock); //去通知 usleep(789789); } return NULL; } void* Consume(void* arg) { (void*)arg; //负责把交易场所中的数据获取出来 while (1) { pthread_mutex_lock(&lock); //每次取最后一个元素 //注意判空,因为快,所以等 //这里用while 的原因是:pthread_cond_wait()不一定返回的是其他线程的signal //有可能被信号打断 while(data.empty()) { //1.释放锁 //2.等待条件就绪(其他线程调用 pthread_cond_signal) //1,2原子的 //3.条件就绪了,重新获取锁 //加上wait的意义:没有数据,消费者数据不会空转,节省了资源。 pthread_cond_wait(&cond, &lock); } int result = data.back(); data.pop_back(); printf("result = %d\n", result); pthread_mutex_unlock(&lock); usleep(123123); } return NULL; } int main() { pthread_mutex_init(&lock, NULL);//互斥锁初始化函数 pthread_cond_init(&cond, NULL);//条件变量初始化函数 pthread_t tid1, tid2; pthread_create(&tid1, NULL, Product, NULL); //创建新线程 pthread_create(&tid2, NULL, Consume, NULL); pthread_join(tid1,NULL); //线程等待 pthread_join(tid2, NULL); pthread_cond_destory(&cond);//条件变量释放函数 pthread_mutex_destory(&lock); return 0; } //不加互斥锁: //但是使用过多线程,就有可能有段错误。 //因为C++STL中所提供的容器和算法都是线程不安全[重要] //加上互斥锁:保证线程安全。
这是最简单的生产者—消费者模型。
总结:一二三
- 一:一个交易场所
- 二:两个角色
- 三:三种关系(生产者之间—互斥关系;消费者之间—互斥关系;生产者和消费者—互斥同步关系)
代码示例2:(队列实现,信号量实现互斥同步)
信号量:
就是一个计数器,表示资源的个数。
- p 申请资源,计数 -1
- v 释放资源,计数器 +1
- 当计数器是 0,再去 p 操作就会阻塞
用信号量:
表示互斥:P V 操作在同一个函数中
表示同步:P V 操作不在同一个函数中
#pragma once #include <stdio.h> #include<pthread.h> //头文件 #include <unistd.h> //sleep头文件 #include <vector> //同步互斥不一定非要用互斥锁和条件变量 //信号量:就是一个计数器,表示资源的个数。 //p 申请资源,计数 - 1 //v 释放资源,计数器 + 1 //当计数器是 0,再去 p 操作就会阻塞 //信号量表示互斥比较简单,同步就很复杂。 //初始化信号量: #include <semaphore.h> //信号量头文件 sem_t sem; //阻塞队列 //一般是由上限的:队列为空,执行 Pop 会阻塞 //队列满了,执行 Push 会阻塞 template<typename T> class BlockingQueue { public: BlockingQueue(int max_size) //构造函数 :max_size_(max_size),head_(0),tail_(0),size_(0), queue_(max_size){ //queue(max_size):含义是将元素个数设置为 max_size sem_init(&lock_, 0, 1); //初始化信号量 sem_init(&elm_, 0, 0); //初始化信号量 sem_init(&blank_, 0, max_size); //初始化信号量 } ~BlockingQueue() { //析构函数 sem_destory(&lock_); sem_destory(&elm_); sem_destory(&blank_); } void Push(const T& data) { //每次插入元素先申请空格资源,没有空格资源,信号量0,说明满了,不能插入,push中阻塞。 sem_wait(&blank_); sem_wait(&lock_); queue_[tail_] = data; ++head_; ++size_; sem_post(&lock_); sem_post(&elm_); //元素资源 +1 } //data 表示出队列的这个元素 void Pop(T* data) { //每次删除元素先申请元素资源,没有元素资源,信号量0,说明为空,不能删除,pop中阻塞。 sem_wait(&elm_); sem_wait(&lock_); *data = queue_[head_]; ++head_; --size_; sem_post(&lock_); //这和互斥锁没区别 sem_post(&blank_); //空格资源 +1 } private: std::vector<T> queue_; int head_; int tail_; int size_; int max_tail; sem_t lock_; //信号量 sem_t elm_; //元素个数 sem_t blank_; //空格个数 }; //用一个二元信号量(非0 或 1)表示互斥锁 //一个信号量表示当前队列中元素的个数 //一个信号量表示当前队列中空格的个数, //插入元素就是消耗一个空格资源,释放一个元素资源 //删除元素消耗一个元素资源,释放一个空格资源 /********************************** ***********以上都在头文件中 **********************************/ #include"BlockQueue.hpp" BlockingQueue<int> queue(100); //两个角色:生产者/消费者——两个线程 void* Product(void* arg) { (void*)arg; int count = 0; //负责把数据送到的交易场所中 while (1) { queue.Push(++count); usleep(789789); } return NULL; } void* Consume(void* arg) { (void*)arg; //负责把交易场所中的数据获取出来 while (1) { int count = 0; queue.Pop(&count); printf("count = %d\n", result); usleep(123123); } return NULL; } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, Product, NULL); //创建新线程 pthread_create(&tid2, NULL, Consume, NULL); pthread_join(tid1, NULL); //释放资源 pthread_join(tid2, NULL); return 0; }
结果就会逐条打印:count = 1 ……;重点理解这个过程,方法模型。
生产者消费者模型实现——线程池
/************************************************************************/ /*在头文件中 */ /************************************************************************/ #pragma once #include <stdio.h> #include "Blockingqueue.hpp" //之前的队列 #include <vector> #include<unistd.h> class Task { public: virtual void Run() { //虚函数 printf("base Run\n"); } Task(); ~Task() { } protected: private: }; //线程池启动的时候会创建一组进程 //每个线程都需要完成一定的任务(执行一定的代码逻辑,这个逻辑调用者来决定) //任务就是一段代码,可以用函数来表示 class ThreadPool { public: //n 表示创建线程的数量 ThreadPool(int n) :queue_(100){ //创建出若干线程 for (int i = 0; i < worker_count_;++i) { pthread_t tid; pthread_creat(&tid, NULL, ThreadEntry, this); worker_.push_back(tid); } } virtual ~ThreadPool(){ //先让线程退出,然后回收 for (size_t i = 0; i < workers_.size(); ++i) { pthread_cancel(workers_[i]); } for (size_t i = 0; i < workers_.size();++i) { pthread_join(workers_[i], NULL); } } //使用线程池的时候,就需要调用者加入一些任务,让线程池去执行 void AddTask(Task* task) { //添加任务函数 queue_.Push(task); } private: BlockingQueue<Task*> queue_; int worker_count_; std::vector<pthread_t> workers_; static void* ThreadEntry(void* arg) { ThreadPool* pool = (ThreadPool*)arg; while (true) { //循环中尝试从阻塞队列中获取到一个任务,并且执行 Task* task = NULL; pool->queue_.Pop(&task); //表面是是task*,实际指向 Mytask* //执行子类,用户自定义的逻辑 task->Run(); delete task; } } }; /************************************************************************/ /* */ /************************************************************************/ #include "threadpool.hpp" //这个类是用户自定制,需要依赖那个数据自定义添加修改 class MyTask:public Task { public: MyTask(int id) :id_(id) { } ~MyTask() { } void Run() { //执行用户自定义的逻辑 printf("id =%d\n",id_); } private: int id_; }; int main() { ThreadPool pool( 10); for (int i = 0; i < 20;++i) { pool.AddTask(new MyTask(i)); } while (1) { Sleep(1); } return 0; }
结果是:打印id =0 ~20的结果
线程池的好处:(和单个线程使用比较)
- 提前把线程创建好,避免反复的创建销毁线程的开销
- 线程不必创建太多,复用一个线程完成多个任务