文章目录
一、多线程的同步
1.概念
2.条件变量
2.1条件变量概念
2.2条件变量接口
1.条件变量初始化
2.等待条件满足
3.唤醒等待
3.销毁条件变量
2.3条件变量demo
二、生产消费模型
1.生产消费模型
2.基于BlockQueue的生产者消费者模型
3.基于C++用条件变量和互斥锁实现一个生产消费模型
4.信号量
1.信号量概念
2.信号量接口
1.初始化信号量
2.等待信号量(P操作 --)
3.发布信号量(V操作 ++)
4.销毁信号量
5.环形生产者消费者模型
当一个线程互斥地访问某个变量时,它发现可能再其他线程改变状态之前,它被挂起。
例如一个线程访问队列,发现队列为空,它只能等待。直到其他线程将一个节点加入到队列中,这种情况就需要用到条件变量。
一、多线程的同步
1.概念
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
2.条件变量
2.1条件变量概念
条件变量是线程同步的一种手段,如果只有一个线程,条件不满足,一直等待下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使得原来的不满足条件变的满足,并且友好通知等待在条件变量上的线程。
条件变量不会无缘无故满足,必然牵扯到共享数据的变化,所以一定需要用锁来保护,没有锁就无法安全的获取和修改共享数据
2.2条件变量接口
1.条件变量初始化
int pthread_cond_init(pthread_cond_t * restrict cond,const pthread_condattr_t * restrict attr);参数:cond 要初始化的条件变量attr:NULL
2.等待条件满足
int pthread_cond_wait(pthread_cond_t * restrict cond,pthread_mutex_t * restrict mutex);参数:cond: 要在这个条件变量上等待mutex:互斥量,等待的时候要释放掉这个锁
3.唤醒等待
int pthread_cond_broadcast(pthread_cond_t * cond);
int pthread_cond_signal(pthread_cond_t * cond);
3.销毁条件变量
int pthread_cond_destroy(pthread_cond_t * cond);
2.3条件变量demo
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
#include<string>
using namespace std;const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *active(void *args)
{string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);// 在调用的时候,会自动释放锁cout<<name<<" activing..."<<endl;pthread_mutex_unlock(&mutex);}}
int main()
{pthread_t tids[num];for(int i = 0; i<num;i++){char * name = new char[32];snprintf(name,32,"thread-%d",i+1);pthread_create(tids+i,nullptr,active,name);}sleep(3);while(true){cout<<"main thread wakeup thread"<<endl;pthread_cond_signal(&cond);sleep(1);}for(int i = 0; i<num;i++){pthread_join(tids[i],nullptr);}}
二、生产消费模型
1.生产消费模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
2.基于BlockQueue的生产者消费者模型
在多线程编程中阻塞队列 (Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元 素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程 操作时会被阻塞。
3.基于C++用条件变量和互斥锁实现一个生产消费模型
需求:使用条件变量和互斥锁实现一个生产消费模型。生产消费模型是一个队列,如上图,这里使用stl中的队列,先实现单生产单消费,一个线程负责生产,一个线程负责消费。这两个线程需要访问同一个队列,所以需要一把锁。在线程挂起的时候,还需要有信号告诉线程现在条件满足了,所以要使用两个条件变量,分别通知生产和消费线程转为就绪态。
实现:将这个模型封装成一个类,两个线程去访问的时候,队列非满,就可以生产,往队列里push数据,队列中数据非空,就可以消费,从队列里pop数据。所以需要两个接口push和pop,调试成功后,最后使用多生产多消费实现。实现代码如下:
//blockqueue.hpp 声明,方法,定义在一个文件中const int gcap = 5;
template<class T>
class blockQueue
{public://构造blockQueue(const int cap = gcap):_cap(cap),{pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_consumerCond,nullptr);pthread_cond_init(&_productorCond,nullptr);}bool isFull() {return _q.size() == _cap;}bool isEmpty() { return _q.empty()};//将数据塞进队列 生产void push(const T & in){pthread_mutex_lock(&_mutex);//注意这里不要用if,可能会误唤醒while(isFull()){//在当前的条件下休眠,就注定了要释放锁,让别的线程去竞争锁//休眠,就是被os切走了,醒来之后又要重新申请锁pthread_cond_wait(&_productorCond,&_mutex); }//如果没满,就让他继续生产_q.push(in);//生产之后,让消费者来消费,唤醒消费的线程 再释放自己手中的锁pthread_cond_signal(&_consumerCond);pthread_mutex_unlock(&_mutex);}//队列非空 消费 void pop(){pthread_mutex_lock(&_mutex);//判断队列是否为空while(isEmpty()){//空的话,在当前条件下休眠pthread_cond_wait(&_consumerCond,&_mutex);}//非空 开始消费 并且唤醒生产者 可以生产了pthread_cond_signal(&_productorCond);pthread_mutex_unlock(&_mutex);}//析构~blockQueue(){//释放锁和两个信号量 队列是一个临时变量可以不用在这里释放pthread_mutex_destroy(&mutex);pthread_cond_destroy(&_consumerCond);pthread_cond_destroy(&_productorCond);}private:std::queue<T> _q;int _cap; //队列中的容量pthread_mutex_t _mutex;pthread_cond_t _consumerCond; //消费者对应的条件变量,如果队列中数据为空,waitpthread_cond_t _productorCond; //生产者对应的条件变量,如果队列中数据为满,wait
};
4.信号量
1.信号量概念
POSIX信号量和System V 信号量作用相同,都是用于同步操作,达到无冲突访问共享资源的目的。但是POSIX可以用于线程间同步。信号量本质就是用来描述临界资源中的数量
sem = 1,就只有0/1两种状态,就是互斥锁。
多元信号量:每个线程,在访问对应资源时,先申请信号量。申请成功,就表示现在可以时候该资源。申请失败,就目前无法访问。
2.信号量接口
#include<semaphore.h>
1.初始化信号量
int sem_init(sem_t * sem, int pshared ,unsiged int value);参数:pshared 0 表示线程间共享,非0 表示进程间共享value:信号量初始值
2.等待信号量(P操作 --)
int sem_wait(sem_t * sem);
3.发布信号量(V操作 ++)
int sem_post(sem_t * sem);
4.销毁信号量
int sem_destroy(sem_t * sem);
5.环形生产者消费者模型
环形队列采用数组模拟,用%运算来模拟环状特性。在为空/满的时候要保证游戏规则,在非空非满的时候保证并发。
环形结构起始状态和结束状态都是一样的,不容易判断是空还是满,所以通过加计数器或者标记位来判断。另外也可以预留一个空的位置,作为满的状态。
但是我们现在有信号量这个计数器,就可以进行多线程间的同步过程。
生产者关心这个空间是否满了,消费者关心是否有数据。环形队列只要访问不同的区域,生产和消费行为可以同时进行。
需求:生产消费模型是一个队列,使用数组模拟,同时需要两个线程,生产线程和消费线程。要维护3种关系:生产者和生产者,消费者和消费者,生产者和消费者之间的关系。其中生产者和生产者,需要互斥。消费者和消费者同样互斥。生产者和消费者,需要先生产再消费,所以需要同步。同时,访问同一个队列(共享资源)需要互斥关系。
实现:将队列封装成类,用数组模拟实现。类需要暴露的接口就是push和pop,即实现p操作和v操作。同时要知道这个队列的大小,定义两个信号量,一个是消费者关心的,一个是生产者关心。申请信号量成功之后,也要知道生产和消费此刻对应队列中的位置,就是具体维护哪个区域。即两个下标。申请自己关心的资源,互相V对方的资源
static const int N = 5;template<class T>
class RingQueue
{private:void P(sem_t &s){sem_wait(&s);}void V(sem_t &s){sem_post(&s);}public://构造RingQueue(int num = N):_ring(num),_cap(num){sem_init(&_data_sem,0,0); sem_init(&_space_sem,0,num);//刚开始都为0_c_step = _p_step = 0;}void push(const T &in){//申请P(_space_sem);_ring[_p_step++] = in;_p_step %= _cap;V(_data_sem);}void pop(T * out){P(_data_sem);*out = _ring[_c_step++];_c_step &= _cap;V(_spcae_sem);}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);}private:std::vector<T> _ring;int _cap;sem_t _data_sem;sem_t _space_sem;int _c_step;int _p_step;
};