目录
引入
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
认识条件变量
条件变量是C语言中使用POSIX线程(pthread)库实现线程同步的一种机制。它允许线程在某个条件满足之前进入等待状态,直到其他线程通知它们该条件已改变。
快速认识接口
认识条件变量
测试代码
一次唤醒一个:
一次全部唤醒:
#include <iostream>
#include <string>
#include<unistd.h>
#include <pthread.h>
using namespace std;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;//定义一把全局的锁
pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;//全局条件变量
int num = 5;
void* waitt(void*agv){
string name=static_cast<const char*>(agv);
while(true){
pthread_mutex_lock(&gmutex);//加锁
pthread_cond_wait(&gcond,&gmutex);//条件变量等待,这里就是线程等待的位置
usleep(10000);
cout<<"i am: "<<name<<endl;
pthread_mutex_unlock(&gmutex);//解锁
}
}
int main()
{
pthread_t threads[num];
for (int i = 0; i < num; i++)//创建线程
{
char *name = new char[1024]; // 用来动态分配一个大小为1024字节的字符数组,并将其地址赋给指针name
snprintf(name, 1024, "thread-%d", i + 1); // 注意不用sizeof了因为sizeof(name)为地址字节不是大小了
pthread_create(threads + i, nullptr, waitt, (void *)name);
usleep(10000);
}
sleep(1);
while(true){//唤醒
pthread_cond_broadcast(&gcond);//全部唤醒
//pthread_cond_signal(&gcond);//一次一个
cout<<"唤醒一个线程......"<<endl;
sleep(2);//唤醒的慢些
}
for (int i = 0; i < num; i++)//线程等待
{
pthread_join(threads[i],nullptr);
}
}
生产消费模型
为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
理解
生产消费模型通常是多执行流并发的模型------多个执行流之间怎么进行互斥同步之间如何协同的模型;
供应商和消费者就是线程,超市就是一段内存,方便面就是数据;
未来生产线程将数据放到缓存中,消费者需要的时候从缓存拿;
思考切入点:"321"原则
1. 一个交易场所(特定数据结构形式存在的一段内存空间)
2. 两种角色(生产角色,消费角色)--生产线程,消费线程
3. 3种关系(生产和生产(竞争关系-互斥),消费和消费(互斥--资源少时就竞争了),生产和消费(互斥--生产者在向缓冲区写入数据时,消费者不能同时从缓冲区中读取数据,以免读取错误数据&&同步--消费者消费数据导致没数据就通知生产者放数据))
实现生产消费模型本质就是:通过代码实现321原则,用锁和条件变量(或其他方式)来实现三种关系!
编写生产消费模型
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程 操作时会被阻塞)
那么可以想到进程间通信时用的管道,一个向管道里写另一个从管道里读。管道如果满了,写进程就要阻塞,如果空了,读进程就要阻塞。管道就是典型的阻塞队列--生产消费模型,只不过使用进程来代替;
单生产单消费
那么生产和生产以及消费和消费之间的关系就不用维护了;
简单测试代码
blockqueue.hpp:
#pragma once #include <iostream> #include <string> #include <queue> //使用stl的queue #include <pthread.h> using namespace std; const static int defaultcap = 5; template <typename T> // 引入模板 class blockqueue { private: bool full() { return _block_queue.size() == _max_cap; } bool empty() { return _block_queue.empty(); } public: blockqueue(int cap = defaultcap) : _max_cap(cap) { pthread_mutex_init(&_mutex, nullptr); // 初始化锁 pthread_cond_init(&_p_cond, nullptr); // 初始条件变量 pthread_cond_init(&_c_cond, nullptr); // 初始条件变量 } void Pop(T *out) { // 把队列中的数据带出去 pthread_mutex_lock(&_mutex); // 用的同一把锁,互斥关系 while (empty()) { pthread_cond_wait(&_c_cond, &_mutex); // 在该条件变量下等待 } // 此时走到这里,1.没有满 || 2.被唤醒了 *out = _block_queue.front(); _block_queue.pop(); // 拿出数据了 pthread_mutex_unlock(&_mutex); pthread_cond_signal(&_p_cond); // 消费一个了那么你就可以生产了,队列有空间了 // signal放在unlock之前还是之后都是可以的 } // 那么唤醒可以由他们两个互相唤醒互相 void equeue(const T &in) { // 入队列 pthread_mutex_lock(&_mutex); // 加锁防止向临界区放数据被打扰 while (full()) { // 判断是否为满了 // 满了不能生产,必须等待 // 此时在临界区里,加锁和解锁之间 // pthread_cond_wait在被调用的时候:除了让自己继续排队等待,还会自己释放传入的锁 // 函数返回的时候,不就还在临界区么?那么被返回时必须先参与锁的竞争,重新加上锁,该函数才被返回;那么返回时就有锁了 pthread_cond_wait(&_p_cond, &_mutex); // 在该条件变量下等待 } // 此时走到这里,1.没有满 || 2.被唤醒了 _block_queue.push(in); // 生产到阻塞队列里,此时没解锁所以一定至少有一个数据在队列里 pthread_mutex_unlock(&_mutex); pthread_cond_signal(&_c_cond); // 生产一个了那么你就可以消费了 // 让消费者消费 } ~blockqueue() { pthread_mutex_destroy(&_mutex); // 将锁销毁 pthread_cond_destroy(&_p_cond); // 将局部条件变量销毁 pthread_cond_destroy(&_c_cond); // 将局部条件变量销毁 } private: queue<T> _block_queue; // 临界资源 int _max_cap; // 队列最大容量 pthread_mutex_t _mutex; // 锁 pthread_cond_t _p_cond; // 为生产者提供的条件变量 pthread_cond_t _c_cond; // 为消费者提供的条件变量 };
构建数据(int)
#include"blockqueue.hpp" #include<pthread.h> #include<ctime> #include<unistd.h> void*consumer(void*agv){ blockqueue<int>* bq=static_cast<blockqueue<int>* >(agv); while(true){ sleep(2);//消费的慢些 //1.拿数据 int date=0; bq->Pop(&date);//拿 //2.处理数据 cout<<"consumer-> "<<date<<endl; } } void*productor(void*agv){ srand(time(nullptr)^getpid());//增加随机性 blockqueue<int>* bq=static_cast<blockqueue<int>* >(agv);//两个线程看到同一个阻塞队列 while(true){ //1.构建数据 int date=rand()%10+1;//[1,10] //2.生产数据 bq->equeue(date);//入 cout<<"producter-> "<<date<<endl; } } int main(){ blockqueue<int>* bq=new blockqueue<int>();//使用int类型 pthread_t c,p; pthread_create(&c,nullptr,consumer,bq); pthread_create(&p,nullptr,productor,bq); pthread_join(c,nullptr); pthread_join(p,nullptr); return 0; } // #include <iostream> // #include <string> // #include<unistd.h> // #include <pthread.h> // using namespace std; // pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;//定义一把全局的锁 // pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;//全局条件变量 // int num = 5; // void* waitt(void*agv){ // string name=static_cast<const char*>(agv); // while(true){ // pthread_mutex_lock(&gmutex);//加锁 // pthread_cond_wait(&gcond,&gmutex);//条件变量等待,这里就是线程等待的位置 // usleep(10000); // cout<<"i am: "<<name<<endl; // pthread_mutex_unlock(&gmutex);//解锁 // } // } // int main() // { // pthread_t threads[num]; // for (int i = 0; i < num; i++)//创建线程 // { // char *name = new char[1024]; // 用来动态分配一个大小为1024字节的字符数组,并将其地址赋给指针name // snprintf(name, 1024, "thread-%d", i + 1); // 注意不用sizeof了因为sizeof(name)为地址字节不是大小了 // pthread_create(threads + i, nullptr, waitt, (void *)name); // usleep(10000); // } // sleep(1); // while(true){//唤醒 // pthread_cond_broadcast(&gcond);//全部唤醒 // //pthread_cond_signal(&gcond);//一次一个 // cout<<"唤醒一个线程......"<<endl; // sleep(2);//唤醒的慢些 // } // for (int i = 0; i < num; i++)//线程等待 // { // pthread_join(threads[i],nullptr); // } // }
构建任务(task--类)
#include"blockqueue.hpp" #include"task.hpp" #include<pthread.h> #include<ctime> #include<unistd.h> void*consumer(void*agv){ blockqueue<task>* bq=static_cast<blockqueue<task>* >(agv); while(true){ sleep(2);//消费的慢些 //1.拿数据 task t;//无参构造 bq->Pop(&t);//拿 //2.处理数据 t.excute(); cout<<"consumer-> "<<t.result()<<endl; } } void*productor(void*agv){ srand(time(nullptr)^getpid());//增加随机性 blockqueue<task>* bq=static_cast<blockqueue<task>* >(agv);//两个线程看到同一个阻塞队列 while(true){ //1.构建数据 int x=rand()%10+1;//[1,10] usleep(10000);//让两个数据尽量不一样,休眠一段时间 int y=rand()%10+1;//[1,10] //2.生产数据 task t(x,y);//有参构造 bq->equeue(t);//入 cout<<"producter-> "<<t.debug()<<endl; } } int main(){ blockqueue<task>* bq=new blockqueue<task>(); pthread_t c,p; pthread_create(&c,nullptr,consumer,bq); pthread_create(&p,nullptr,productor,bq); pthread_join(c,nullptr); pthread_join(p,nullptr); return 0; }
task.hpp:
#pragma once #include "blockqueue.hpp" using namespace std; class task { public: task() {} task(int x, int y) : _x(x), _y(y) { } void excute() { _result = _x + _y; } string debug() { string msg = to_string(_x) + "+" + to_string(_y) + "=?"; return msg; } string result() { string msg = to_string(_x) + "+" + to_string(_y) +"="+ to_string(_result); return msg; } ~task() { } private: int _x; int _y; int _result; };
仿函数
#include "blockqueue.hpp" #include "task.hpp" #include <pthread.h> #include <ctime> #include <unistd.h> void *consumer(void *agv) { blockqueue<task_t> *bq = static_cast<blockqueue<task_t> *>(agv); while (true) { sleep(2); task_t t; bq->Pop(&t);// 从队列中取出并执行任务 t();// 执行任务 } } void *productor(void *agv) { srand(time(nullptr) ^ getpid()); // 增加随机性 blockqueue<task_t> *bq = static_cast<blockqueue<task_t> *>(agv); // 两个线程看到同一个阻塞队列 while (true) { bq->equeue(download); cout<<"productor->download "<<endl; } } int main() { blockqueue<task_t> *bq = new blockqueue<task_t>(); pthread_t c, p; pthread_create(&c, nullptr, consumer, bq); pthread_create(&p, nullptr, productor, bq); pthread_join(c, nullptr); pthread_join(p, nullptr); return 0; }
task.hpp:
#pragma once #include<iostream> #include<functional> using namespace std; using task_t=function<void()>;//等价于typedef function<void()> task_t; //定义了一个新的类型别名 task_t,它表示一个接受无参数并返回 void 的函数类型 void download(){ cout<<"i am a download task"<<endl; }
多生产多消费
多生产多消费直接复用上面代码即可;
那么针对生产者不仅仅要将任务放到超市(花费时间),他还要产生任务也要花费时间;
那么针对消费者不仅仅要从超市拿到任务(花费时间),这个任务属于消费者自己了,那么他还要处理任务也要花费时间;
那么我们就不能仅仅只考虑放任务到超市叫生产,拿任务叫消费;
那么一个生产商再放任务的时候,那么其他生产商有没有正在生产任务呢。
那么放任务和产生任务就并发了;
那么一个消费者再拿任务的时候,那么其他消费者有没有早都获取了任务正在处理任务呢。
那么拿任务和处理任务就并发了;
如果未来消费处理任务花费时间比较久但是生产任务比较快,那么可以单生产多消费,那么生产任务的时候,一方面有线程在获取另一方面在并发处理任务;如果未来生产任务花费时间比较久但是消费任务比较快,那么可以多生产单消费;
都很慢就可以多生产多消费;
问题:为什么等待就要在加锁和解锁之间等待呢?无论是生产者还是消费者都必须先检测资源的状态,对于生产和消费来说他们要访问公共资源,它不知道资源的条件是否满足。对于生产者来说希望队列有空间,对于消费者来说希望队列有数据。可是对于他们来说他们并不知道,只有他们查一次才知道,而查这一行为本身就是访问,就决定了查之前就要加锁,并且检测可能满足可能不满足,注定了必须在临界区里等待因为判定结果是在临界区里的;