目录
基于阻塞队列(BlockingQueue)的生产者消费者模型
生产者消费者模型
概念
生活中的生产者消费者模型
生产者消费者的引入, 就是以人们生产生活的典型场景命名的. 最典型的就是我们作为普通的消费者去商场买东西, 那么超市的东西是哪来的呢? 超市的东西并不是超市自己生产的, 而是供应商生产, 送货到商场.
那么在这个典型的关系中, 去买东西的我们就是消费者, 供应商就是生产者, 而商场就只是一个交易场所.
这种消费者与供应商之间没有直接发生交易而是之间多出一个交易场所的模型避免下面几个问题 :
- 供应商生产产品不需要和消费者有直接联系, 消费者购买产品也不需要和供应商有太多联系.
- 当有大量消费者购买但供应商短时间生产不够时, 商场的囤货就可以缓解供应商压力. 当供应商大量生产, 并没有那么多消费者购买时, 商场也可以将产品囤下(起到缓冲作用).
- 当没有商场时, 当出现较多消费者想要购买产品, 那就只能一个一个去供应商跟前买, 或者说供应商只能等着有消费者再生产. 但有商场之后, 消费者可以一起去商场买产品, 供应商也不需要等消费者来买就可以生产产品.
在编程开发中的生产者与消费者模型
对比于上面的例子, 人们把程序中数据生产模块称之为生产者, 把数据处理模块称为消费者, 生产数据的模块与处理数据的模块之间的缓冲场所, 称之为缓冲区
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题. 生产者和消费者彼此之间不直接通讯, 而通过阻塞队列来进行通讯, 所以生产者生产完数据之后不用等待消费者处理, 直接扔给阻塞队列, 消费者不找生产者要数据, 而是直接从阻塞队列里取, 阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力. 这个阻塞队列就是用来给生产者和消费者解耦的 .
生产者和消费者模型的优点 :
- 解耦合 : 生产者模块和消费者模块不直接交互
- 支持忙闲不均 : 队列可以起到缓冲作用
- 支持并发 : 支持并发, 保证线程安全, 当多个消费者和多个生产者对队列操作时, 不会出现问题
生产者与消费者模型的实现 ---- 一个场所, 两种角色, 三种关系
- 一个场所 : 缓冲区(实现为线程安全的阻塞队列)
- 两种角色 : 生产者与消费者
- 三种关系 : 生产者与生产者之间应该互斥
消费者与消费者之间应该互斥
消费者与生产者之间应该同步 + 互斥
基于阻塞队列(BlockingQueue)的生产者消费者模型
BlockingQueue.h
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
#include<iostream>
#include<queue>
#include<pthread.h>
using namespace std;
#endif
#define MAX_QUEUE 5
template<class T>
class BlockingQueue{
queue<T> m_q;//安全队列
size_t m_max_capacity;//最大容量
pthread_mutex_t m_mutex;
pthread_cond_t m_cond_pro;//生产者
pthread_cond_t m_cond_con;//生产者
public:
BlockingQueue(size_t capacity = MAX_QUEUE)
: m_max_capacity(capacity)
{
pthread_mutex_init(&m_mutex, NULL);
pthread_cond_init(&m_cond_con, NULL);
pthread_cond_init(&m_cond_pro, NULL);
}
~BlockingQueue(){
pthread_mutex_destroy(&m_mutex);
pthread_cond_destroy(&m_cond_con);
pthread_cond_destroy(&m_cond_pro);
}
void Push(const T& data);
void Pop(T* data);
};
template<class T>
void BlockingQueue<T>::Push(const T& data){
pthread_mutex_lock(&m_mutex);
while(m_q.size() == m_max_capacity){
pthread_cond_wait(&m_cond_pro, &m_mutex);
}
m_q.push(data);
cout << "生产线程:0x" << pthread_self() << hex <<": 我生产了一份数据\n";//为了测试直观
pthread_mutex_unlock(&m_mutex);
pthread_cond_signal(&m_cond_con);
}
template<class T>
void BlockingQueue<T>::Pop(T* data){
pthread_mutex_lock(&m_mutex);
while(m_q.empty()){
pthread_cond_wait(&m_cond_con, &m_mutex);
}
*data = m_q.front();
m_q.pop();
cout << "消费者线程:0x" << pthread_self() << hex <<": 我拿到了一份数据\n";//为了测试直观
pthread_mutex_unlock(&m_mutex);
pthread_cond_signal(&m_cond_pro);
}
main.cpp
#include<iostream>
#include<cstdio>
#include<cstring>
#include<pthread.h>
#include"BlockingQueue.h"
#define MAX_THR 5
void* producer(void* arg){
BlockingQueue<int>* q = (BlockingQueue<int>*)arg;
while(1){
q->Push(1);
//printf("生产线程%p: 我生产了一份数据\n", pthread_self());
}
return NULL;
}
void* consumer(void* arg){
BlockingQueue<int>* q = (BlockingQueue<int>*)arg;
int data;//从阻塞队列中拿到的数据
while(1){
q->Pop(&data);
//printf("消费者线程%p: 我拿到了一份数据\n", pthread_self());
}
return NULL;
}
int main(){
BlockingQueue<int> q(10);
pthread_t consumer_tid[MAX_THR], producer_tid[MAX_THR];
int ret;
for(int i = 0; i < MAX_THR; ++i){
ret = pthread_create(&producer_tid[i], NULL, producer, (void*)&q);
if(ret){
fprintf(stderr, "producer thread%d create:%s\n", i + 1, strerror(ret));
return -1;
}
}
for(int i = 0; i < MAX_THR; ++i){
ret = pthread_create(&consumer_tid[i], NULL, consumer, (void*)&q);
if(ret){
fprintf(stderr, "consumer thread%d create:%s\n", i + 1, strerror(ret));
return -1;
}
}
for(int i = 0; i < MAX_THR; pthread_join(consumer_tid[i], NULL), ++i);
for(int i = 0; i < MAX_THR; pthread_join(producer_tid[i], NULL), ++i);
return 0;
}
POSIX信号量
POSIX信号量用于进程间或线程间的同步与互斥
本质 : 非负的整数计数器 + 等待队列 + 向外提供的阻塞/唤醒的功能接口
计数器 : 对资源进行计数, 统计当前资源数量, 通过自身的计数, 就可以进行条件判断, 是否能够进行操作, 若不能获取资源, 则阻塞当前执行流.
有了条件变量和互斥锁为什么还要有信号量?
- 条件变量需要外部变量搭配互斥锁来实现, 而计数器只需要通过自身计数
- 由于互斥锁的粒度比较大,在多个线程如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
在程序初始化阶段, 根据实际的资源数量初始化信号量的计数器数值, 在每次获取资源之前, 先获取信号量(先判断计数是否大于0. 若大于, 则计数-1, 直接返回, 获取数据, 若 == 0, 则阻塞), 当其它执行流生产一个资源后, 先判断计数器是否 == 0, 若 == 0, 则唤醒一个执行流, 然后进行计数+1.
头文件 : #include <semaphore.h>
POSIX信号量接口
信号量类型 : sem_t
初始化
原型 : int sem_init(sem_t* sem, int pshared, unsigned int value)
功能 :
参数 : sem : 要初始化的信号量的地址
pshared : 传入0则代表信号量是在线程间共享(用于线程间), 非0则是在进程间共享(用于进程间)
value: 初始化信号量计数器的初始值(只能>=0)返回值 : 成功返回0, 失败返回-1, 并设置errno
注意 : 初始化一个已经初始化的信号量会导致未定义的行为
销毁
原型 : int sem_destroy(sem_t* sem)
功能 : 销毁不再使用的信号量
参数 : sem : 传入不再使用要销毁的信号量的地址
返回值 : 成功返回0, 失败返回-1, 并设置errno
注意 : 初始化一个已销毁过的信号量会导致未定义的行为
销毁一个正在阻塞的信号量会导致未定义的行为
等待(P操作, -1)
原型 : int sem_wait(sem_t* sem)
功能 : 锁定sem指向的信号量, 如果计数的值大于0, 则计数-1, 函数立即返回. 如果信号量当前的值为0, 则阻塞直到其他的
执行流将计数+1, 唤醒这个阻塞的执行流返回值 : 成功返回0, 失败返回-1, 并设置errno
原型 : int sem_trywait(sem_t *sem)
功能 : 锁定sem指向的信号量, 如果计数的值大于0, 则计数-1, 函数立即返回. 如果信号量当前的值为0, 则返回-1(不阻塞),
并设置errno返回值 : 成功返回0, 失败返回-1, 并设置errno
原型 : int sem_timedwait(sem_t* sem, const struct timespec* abs_timeout);
功能 : 限时尝试对信号量加锁, 即在一定时间内阻塞等待, 当超过时限, 直接返回-1, 并设置errno(也就是说并不会像wait一样可能一直阻塞, 也不会像trywait一样, 一定不阻塞)
参数 : abs_timeout : 绝对时间
struct timespec是一个结构体. 设置1秒为例 :time_t cur = time(NULL);//获取当前时间 struct timespec t; //定义timespec 结构体变量t t.tv_sec = cur + 1;//定时1秒 t.tv_nsec = t.tv_sec +100; sem_timedwait(&sem, &t); 传参
返回值 : 成功返回0, 失败返回-1, 并设置errno
唤醒(V操作, +1)
原型 : int sem_post(sem_t* sem)
功能 : 递增(解锁)sem指向的信号量. 如果计数的值因此变得大于零, 则唤醒另一个进程或线程, 阻塞将被唤醒并继续锁定信号量
返回值 : 成功返回0, 失败返回-1, 并设置errno
信号量实现的环形队列的生产者消费者模型
RingQueue.h
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
#include<iostream>
#include<vector>
#include<pthread.h>
#include<semaphore.h>
using namespace std;
#endif
#define MAX_QUEUE 5
template<class T>
class RingQueue{
vector<T> m_array;
size_t m_max_capacity;
int m_pos_real;
int m_pos_front;
sem_t m_sem_data;//数据资源计数器
sem_t m_sem_free;//空闲空间计数器
sem_t m_sem_lock;//实现互斥锁
public:
RingQueue(size_t max_capacity = MAX_QUEUE):
m_array(max_capacity),
m_max_capacity(max_capacity),
m_pos_real(0),
m_pos_front(0) {
sem_init(&m_sem_data, 0, 0);
sem_init(&m_sem_free, 0, max_capacity);
sem_init(&m_sem_lock, 0, 1);
}
~RingQueue(){
sem_destroy(&m_sem_data);
sem_destroy(&m_sem_free);
sem_destroy(&m_sem_lock);
}
void Push(const T& data);
void Pop(T* data);
};
template<class T>
void RingQueue<T>::Push(const T& data){
sem_wait(&m_sem_free);//通过自身计数判断, 是否有剩余空间, 若为0则阻塞,
//若>0则计数 - 1
sem_wait(&m_sem_lock);//先加锁, 保护入队操作, 计数为1则不阻塞, 计数-1, 否则阻塞
m_array[m_pos_real] = data;
m_pos_real = (m_pos_real + 1) % m_max_capacity;
cout << "生产线程:0x" << pthread_self() << hex <<": 我生产了一份数据\n";//为了测试直观
sem_post(&m_sem_lock);//解锁, 计数+1, 并唤醒等待锁的生产者
sem_post(&m_sem_data);//数据资源计数+1, 并唤醒消费者
}
template<class T>
void RingQueue<T>::Pop(T* data){
sem_wait(&m_sem_data);
sem_wait(&m_sem_lock);
*data = m_array[m_pos_front];
m_pos_front = (m_pos_front + 1) % m_max_capacity;
cout << "消费者线程:0x" << pthread_self() << hex <<": 我拿到了一份数据\n";//为了测试直> 观
sem_post(&m_sem_lock);
sem_post(&m_sem_free);
}
main.cpp
#include<iostream>
#include<cstdio>
#include<cstring>
#include<pthread.h>
#include"RingQueue.h"
#define MAX_THR 5
void* producer(void* arg){
RingQueue<int>* p = (RingQueue<int>*)arg;
while(1){
p->Push(1);
// printf("生产线程:%p: 我生产了一份数据\n", pthread_self());
}
return NULL;
}
void* consumer(void* arg){
RingQueue<int>* p = (RingQueue<int>*)arg;
int data;
while(1){
p->Pop(&data);
//printf("消费者线程%p: 我拿到了一份数据\n", pthread_self());
}
return NULL;
}
int main(){
RingQueue<int> rq;
pthread_t con_tid[MAX_THR], pro_tid[MAX_THR];
int ret;
for(int i = 0; i < MAX_THR; ++i){
ret = pthread_create(&pro_tid[i], NULL, producer, (void*)&rq);
if(ret){
fprintf(stderr, "producer thread%d create:%s\n", i + 1, strerror(ret));
return -1;
}
}
for(int i = 0; i < MAX_THR; ++i){
ret = pthread_create(&con_tid[i], NULL, consumer, (void*)&rq);
if(ret){
fprintf(stderr, "consumer thread%d create:%s\n", i + 1, strerror(ret));
return -1;
}
}
for(int i = 0; i < MAX_THR; pthread_join(con_tid[i], NULL), ++i);
for(int i = 0; i < MAX_THR; pthread_join(pro_tid[i], NULL), ++i);
return 0;
}
读者写者模型
我们还是从现实中的例子来看, 一部网络小说, 我们阅读的人可能有成千上万, 但能修改它的人却只有很少的人. 作者还在紫写的部分我们读者还不能看, 但当写好之后, 我们读者是可以一起阅读的, 并不会存在我看完了你再看的情况 (例子可能并不恰当, 但足以说明问题).
在我们编程开发中, 也会有存在这种现象, 当有多个执行流访问临界资源时, 可能会造成数据混乱等问题, 我们用同步+互斥来解决, 但当出现大量的只读执行流, 存在少量的写执行流时, 如果每个读执行流访问临界资源时, 其他执行流(包括读写)都不能访问的话, 就大大拖慢了效率, 因为大部分的执行流都不修改临界资源, 它们是可以并发的, 我们用读写锁来解决这种问题.
读者写者模型典型场景 ---- 少量写临界资源的执行流, 大量只读临界资源的执行流 .
解决方法 : 写互斥, 读共享
读写锁的实现原理
读锁(一个读者计数):若>0, 则表示有读执行流正在读访问, 想加写锁的执行流就需要阻塞等待, 想加读锁的执行流可以继续加
写锁(一个写者计数) : 若>0, 则表示有写执行流正在写访问, 想要加写锁和读锁的执行流都需要阻塞等待.
其中不能加锁时的等待, 通过自旋锁实现
自旋锁
自旋锁 :循环判断条件, 并且强占CPU(一直处于运行状态, 不断的在判断条件是否满足. CPU不可被剥夺), 比较消耗CPU, 比较适用于等待时间比较短的操作
为什么自旋锁要一直占用CPU循环判断呢 ?
如果持有锁的线程能在短时间内释放锁资源, 那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态, 这样就避免了内核态与用户态频繁来回切换的性能消耗.
读写锁的接口
读写锁类型 : pthread_rwlock_t
初始化
原型 : int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr)
功能 : 初始化读写锁
参数 : rwlock : 需要初始化的读写锁变量
attr : 读写锁的属性, 也就是优先级, 传入NULL是默认(读优先), 其他属性需单独设置变量pthread_rwlockattr变量, 传
入其指针返回值 : 成功返回0, 失败返回值>0, 返回的是errno错误码
设置读写优先级
读写锁属性对象类型 : pthread_rwlockattr_t
- 初始化
原型 : pthread_rwlockattr_init(pthread_rwlockattr_t * attr)
功能 : 初始化读写锁属性对象
返回值 : 成功返回0, 错误返回值>0, 返回errno
- 设置优先级
原型 : int pthread_rwlockattr_setkind_np ( pthread_rwlockattr_t* attr, int pref)
功能 : 设置读写锁的优先属性
参数 : attr : 需要设置的读写锁的地址
pref : 属性, 可以设置为以下值
PTHREAD_RWLOCK_PREFER_READER_NP : 默认属性(读锁优先)
PTHREAD_RWLOCK_PREFER_WRITER_NP : 写者优(该属性有bug, 表现与默认属性一致)
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP : 写者优先(不可以递归加锁)返回值 : 成功返回0, 失败返回值>0, 返回的是errno错误码
- 销毁
原型 : int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr)
功能 : 销毁一个读写锁属性变量, 销毁之后可以重新初始化再使用
返回值 :成功返回0, 失败返回值>0, 返回的是errno错误码
例 : 设置一个写优先的读写锁
pthread_rwlockattr_t attr; pthread_rwlockattr_init(&attr); pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP); pthread_rwlock_init(&rwlock, &attr); pthread_rwlockattr_destroy(&attr);
设置一个读优先
pthread_rwlock_init(&rwlock, nullptr);
加读锁
原型 : int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
功能 : 如果写执行流不持有锁, 并且锁没有阻塞写执行流, 那么当前执行流调用该接口将获得读锁, 否则则阻塞.
参数 : rwlock : 需要加读锁的锁变量的地址
返回值 : 成功返回0, 失败返回值>0, 返回的是errno码
加写锁
原因 : int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
功能 : 如果没有其他(读或写)执行流持有读写锁, 则当前执行流调用该接口将获得写锁, 否则阻塞
参数 : rwlock : 需要加写锁的锁变量的地址
返回值 : 成功返回0, 失败返回值>0, 返回的是errno码
解锁
原型 : int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
功能 : 读锁写锁加锁后都用这个接口释放做资源.
如果有不止一个读锁已经锁定, 那么该接口调用只会将其读者计数-1, 读锁状态不会发生改变, 直到读者计数为1时, 调用该接口, 锁变量处于无持有者状态;
如果要释放一个处于加锁状态的写锁资源, 调用该接口后锁变量处于无持有者状态 .返回值 : 成功返回0, 失败返回值>0, 返回的是errno码
销毁
原型 : int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
功能 : 销毁一个读写锁变量
返回值 : 成功返回0, 失败返回值>0, 返回的是errno错误码
#include<iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
volatile int data = 1000;
pthread_rwlock_t rwlock;
void * reader(void * arg) {
while (1) {
pthread_rwlock_rdlock(&rwlock);
if (data == 0) {
pthread_rwlock_unlock(&rwlock);
break;
}
printf("reader thread%d: %d\n", arg, data);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
void * writer(void * arg) {
while (1) {
pthread_rwlock_wrlock(&rwlock);
if (data == 0) {
pthread_rwlock_unlock(&rwlock);
break;
}
printf("writer thread%d: %d\n", arg, --data);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
void init_readers(vector<pthread_t>& vec) {
for (size_t i = 0; i < vec.size(); ++i) {
pthread_create(&vec[i], nullptr, reader, (void *)i);
}
}
void init_writers(vector<pthread_t>& vec) {
for (size_t i = 0; i < vec.size(); ++i) {
pthread_create(&vec[i], nullptr, writer, (void *)i);
}
}
void join_threads(vector<pthread_t> const& vec){
for (vector<pthread_t>::const_reverse_iterator it = vec.rbegin(); it !=
vec.rend(); ++it) {
pthread_join(*it, nullptr);
}
}
void init_rwlock() {
#if 1
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿
pthread_rwlock_init(&rwlock, nullptr);
#endif
}
int main() {
//测试效果不明显的情况下,可以加大 reader_nr
//但也不能太大,超过一定阈值后系统就调度不了主线程了
const size_t reader_nr = 1000;
const size_t writer_nr = 3;
vector<pthread_t> readers(reader_nr);
vector<pthread_t> writers(writer_nr);
init_rwlock();
init_readers(readers);
init_writers(writers);
join_threads(writers);
join_threads(readers);
pthread_rwlock_destroy(&rwlock);
}
相关博客, 戳链接( ̄︶ ̄)↗ : Linux 多线程(线程概念/特点/优缺点/与进程比较)
Linux 多线程(线程控制(创建/终止/等待/分离))
Linux 多线程之线程安全(同步与互斥/互斥锁/条件变量/死锁/)