目录
生产者消费者模型
条件变量
接口
实现基于阻塞队列的cp模型
POSIX信号量
信号量的PV操作
接口:
初始化信号量:
销毁信号量
等待信号量
发布信号量
基于环形队列的生产者消费者模型
线程池
应用场景:
如何实现?
生产者消费者模型
生产者消费者模型的作用:生产者消费者模式本质就是通过一个容器来解决生产者消费者强耦合的问题。
这个容器可以是队列或链表,充当着缓冲区的作用 ,生产者和消费者彼此不直接通讯,而通过阻塞队列来通讯,生产者生产完数据不用等待消费者获取,而是直接放入阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列中获取数据,阻塞队列平衡了生产者消费者的处理能力,将生产者和消费者解耦。
生产者消费者模型是同步与互斥的典型应用场景。
生产者消费者模型优点解耦支持并发支持忙闲不均
队列或链表在内存中是消费者和生产者之间的缓冲区。
缓冲区作用是提高生产消费效率和将生产者消费者解耦,同时缓冲区是临界资源,操作时需要加锁保护。
这个模型下有一个交易场所(缓冲区),两种角色(生产者和消费者),三种关系。(简记321原则)
三种关系
生产者之间:互斥
消费者之间:互斥
消费者与生产者之间:互斥,同步
为什么存在互斥关系?
因为生产线程和消费线程都会访问缓冲区,缓冲区是临界资源,生产者消费者都会对临界资源做操作,所以访问临界资源需要加锁保护,所以无论是生产线程还是消费线程访问之前都需要先申请锁,所以生产者之间,消费者之间,消费者与生产者之间都需要竞争锁,所以它们都具有互斥关系。
为什么消费者与生产者之间存在同步关系?
当生产者生产满了数据在队列中,就不能继续生产,否则生产者一直占有锁,就会造成消费者饥饿问题,当消费者消费完了队列中的数据,队列为空就不能继续消费,消费者一直占有锁会造成生产者饥饿问题,所以应该让消费者和生产者访问队列具有一定的顺序性,比如队列空了就先生产,队列满了就先消费,这个就是线程之间的同步关系。
那么如何让多个生产者,消费者在我们指定的条件下等待或唤醒?
使用条件变量。
条件变量
接口
条件变量特点:唤醒线程由系统唤醒变成让程序员唤醒
初始化条件变量:pthread_cond_init
销毁条件变量:pthread_cond_destroy
初始化方法与pthread_mutex_t相似,可以使用宏初始化全局变量,也可以使用函数初始化。
让线程阻塞等待条件就绪:
第一参数是条件变量,表示指定条件。
第二个参数是锁,是线程阻塞等待时会释放的锁,线程被唤醒时被申请的锁。
pthread_cond_wait在特定条件下阻塞等待
pthread_cond_timedwait等待指定时间函数返回
pthread_cond_timedwait和pthread_cond_wait的区别是 pthread_cond_timedwait会阻塞等待abstime长的时间,到时后线程如果没被唤醒函数就会返回错误码。
注意:
处于等待条件下不能被pthread_cancel。
当线程在条件未就绪等待时,线程阻塞等待时线程会自动释放锁,当线程被唤醒时会自动申请锁。
唤醒指定条件下等待的线程:
pthread_cond_signal:唤醒一个在指定条件等待的线程
pthread_cond _broadcast:唤醒所有在指定条件下等待的线程
唤醒线程函数在唤醒多个线程类似于排队,多个线程会被按一定顺序依次唤醒。
实现基于阻塞队列的cp模型
阻塞队列与普通队列区别:当队列为空的时候,从队列获取元素的操作将会被阻塞,当队列为满的时候,往队列存放元素的操作也会被阻塞,类似管道。
实现单生产者,单消费者版本:
封装blockQueue类
队列
容量:队列最大数据个数
互斥锁
条件变量:保持生产者消费者同步,避免生产饥饿或消费饥饿问题
生产接口
加锁
判断队列是否满了
满了,不生产,在条件下等待(休眠)
不满,生产,唤醒消费者
生产
解锁
消费接口
加锁
判断
空,不消费,在条件下等待(休眠)
不空,消费,唤醒生产者
消费
解锁
注意:
线程被唤醒了并不代表条件一定满足(伪唤醒或阻塞等待调用失败等),需要再检测条件是否满足
在临界区唤醒线程或临界区外唤醒线程都行
代码实现:
BlockQueue.h:
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
using namespace std;
template <class T>
class BlockQueue
{
private:
queue<T> q_;
int capacity_;
pthread_mutex_t mutex_;
pthread_cond_t full_;
pthread_cond_t empty_;
bool isFull() { return q_.size() == capacity_; }
bool isEmpty() { return q_.empty(); }
public:
BlockQueue(int capacity)
: capacity_(capacity)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&full_, nullptr);
pthread_cond_init(&empty_, nullptr);
}
void push(T &t)
{
pthread_mutex_lock(&mutex_);
while (isFull()) // 当队列满的时候,生产线程在full_条件下等待。
{
pthread_cond_wait(&full_, &mutex_); // 函数在线程阻塞的时候会自动释放锁,在线程被唤醒的时候会自动申请锁
}
q_.push(t);
pthread_mutex_unlock(&mutex_);
pthread_cond_signal(&empty_); // 生产者生产完数据,唤醒在empty_条件下等待的消费者
}
void pop(T &data) // 输出型参数
{
pthread_mutex_lock(&mutex_);
while (isEmpty()) // 当队列空的时候,消费线程在empty_条件下等待。
{
pthread_cond_wait(&empty_, &mutex_); // 函数在线程阻塞的时候会自动释放锁,在线程被唤醒的时候会自动申请锁
}
data = q_.front();
q_.pop();
pthread_mutex_unlock(&mutex_);
pthread_cond_signal(&full_); // 消费者消费完数据,唤醒在full_条件下等待的生产者
}
};
测试代码:
#include "BlockQueue.h"
void *pRoutine(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
while (1)
{
int data = rand() % 1000;
bq->push(data);
cout << "生产者生产了数据:" << data << endl;
}
return nullptr;
}
void *cRoutine(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
while (1)
{
int data = -1;
bq->pop(data);
cout << "消费者消费了数据:" << data << endl;
sleep(1);
}
return nullptr;
}
int main()
{
BlockQueue<int> bq(5);
pthread_t p, c;
pthread_create(&p, nullptr, pRoutine, &bq);
pthread_create(&c, nullptr, cRoutine, &bq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
return 0;
}
输出:
可以看出一开始队列为空,生产者一次性生产满了队列,队列满了就不生产了, 而后消费者消费一个数据生产者生产一个数据,也就是生产者消费者保持同步了。
支持并发: 在队列中的访问线程们一定是串行的,支持并发主要是体现在生产者们同时制作任务,消费者们同时消费任务。支持忙闲不均: 指的是任务放入队列前和从队列获取任务后,生产线程和消费线程各自做着自己的工作。
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
信号量本质:信号量是一个计数器——描述临界资源数量的计数器
申请信号量的预定机制:只要信号量申请成功,就一定会获得指定的资源。
当临界资源数量就一个,信号量要么为0要么为1,就两种状态称为二元信号量,即互斥锁。
信号量的PV操作
P操作:申请信号量,即申请临界资源,将信号量--。
V操作:释放信号量,即释放临界资源,将信号量++。
多线程在申请信号量,同时访问信号量,所以信号量是临界资源,所以操作临界资源必须是原子操作才不出问题,即PV操作需要是原子操作,这点已经由内核实现保证了。
接口:
头文件:semaphore.h
初始化信号量:
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
int sem_wait(sem_t *sem);
即信号量的P操作,申请信号量
发布信号量
int sem_post(sem_t *sem);
即信号量的V操作,释放信号量
基于环形队列的生产者消费者模型
我们知道用数组实现环形队列会用两个头尾指针,当队列为满或空,指针指向同一个位置 ,那么如何分辨这两种状态呢?
答案是不需要分辨,使用信号量帮我们维护就行了。
除了上述两种情况,其他时候指针指向不同的位置,这时生产线程消费线程是并发的,因为可以让生产者和消费者同时访问队列的不同区域,生产的同时也在消费。
实现时还需要两个原则:
原则一:生产者和消费者不能对同一个位置进行访问,因为生产者消费者是互斥的。
这个原则信号量可以保证。
对于生产者而言:关心空间(room),空间一开始为N(环形队列大小)
对于消费者而言:关心数据(data),数据一开始为0,起初没有数据
原则二:
队列为空:消费者不能超过生产者,没有数据时不能消费,要让生产者先运行
队列为满:生产者不能把消费者套圈,即生产者不能继续写入 ,要让消费者先运行
程序员编码实现:不同线程访问临界资源的不同区域
代码实现:
RingQueue.h:
#pragma once
#include <semaphore.h>
#include <vector>
#include <iostream>
#include <unistd.h>
#define CAPACITY 8
using namespace std;
template <class T>
class RingQueue
{
public:
RingQueue(uint32_t capacity = CAPACITY)
: _ringQueue(capacity), _capacity(capacity), _pIndex(0), _cIndex(0)
{
sem_init(&_roomSem, 0, _capacity);
sem_init(&_dataSem, 0, 0);
}
~RingQueue()
{
sem_destroy(&_roomSem);
sem_destroy(&_dataSem);
}
void push(T &data) // 生产者生产数据
{
sem_wait(&_roomSem); // 申请空间资源,--roomSem
_ringQueue[_pIndex] = data;
sem_post(&_dataSem); // 释放数据资源,++dataSem
//++_pIndex放后面做,因为上面是临界区,尽量不在临界区做多余的事情
_pIndex++;
_pIndex %= _capacity;
}
void pop(T &data) // 消费者消费数据,输出型参数
{
sem_wait(&_dataSem); // 申请数据资源,--dataSem
data = _ringQueue[_cIndex];
sem_post(&_roomSem); // 释放空间资源,++roomSem
//++_cIndex放后面做,因为上面是临界区,尽量不在临界区做多余的事情
++_cIndex;
_cIndex %= _capacity;
}
private:
vector<T> _ringQueue; // 环形队列
uint32_t _capacity; // 环形队列大小
sem_t _roomSem; // 空间资源个数
sem_t _dataSem; // 数据资源个数
uint32_t _pIndex; // 表示生产者正在生产的位置
uint32_t _cIndex; // 表示消费者正在消费的位置
};
RingQueue.cc:
#include "RingQueue.h"
void *pRoutine(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (1)
{
int data = rand() % 1000;
rq->push(data);
cout << "生产者生产了一个数据:" << data << endl;
}
return nullptr;
}
void *cRoutine(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (1)
{
int data = -1;
rq->pop(data);
cout << "消费者消费了一个数据:" << data << endl;
sleep(1);
}
return nullptr;
}
int main()
{
RingQueue<int> rq(6);
pthread_t p, c;
pthread_create(&p, nullptr, pRoutine, &rq);
pthread_create(&c, nullptr, cRoutine, &rq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
return 0;
}
运行结果:
如果放慢生产节奏,消费者也会跟着变慢,这是同步的体现。
倘若要实现多生产多消费者,关键是要让生产者之间互斥,消费者之间互斥,只需要生产者消费者分别加锁即可。
注意:
在申请信号量之后加锁
在释放信号量之前解锁
为什么如此加锁?因为处理数据或任务要花时间,任何时候访问队列只有一个生产线程和消费线程,我们可以利用信号量的预订机制让线程先申请信号量,即先预订资源效率更高。
代码实现:
RingQueue.h:
#pragma once
#include <semaphore.h>
#include <vector>
#include <iostream>
#include <unistd.h>
#define CAPACITY 8
using namespace std;
template <class T>
class RingQueue
{
public:
RingQueue(uint32_t capacity = CAPACITY)
: _ringQueue(capacity), _capacity(capacity), _pIndex(0), _cIndex(0)
{
sem_init(&_roomSem, 0, _capacity);
sem_init(&_dataSem, 0, 0);
pthread_mutex_init(&_pmutex,nullptr);
pthread_mutex_init(&_cmutex,nullptr);
}
~RingQueue()
{
sem_destroy(&_roomSem);
sem_destroy(&_dataSem);
pthread_mutex_destroy(&_pmutex);
pthread_mutex_destroy(&_cmutex);
}
void push(T &data) // 生产者生产数据
{
sem_wait(&_roomSem); // 申请空间资源,--roomSem
pthread_mutex_lock(&_pmutex);
_ringQueue[_pIndex] = data;
_pIndex++;
_pIndex %= _capacity;
cout << "生产者[" << pthread_self() << "]:生产了一个数据:" << data << endl;//便于观察
pthread_mutex_unlock(&_pmutex);
sem_post(&_dataSem); // 释放数据资源,++dataSem
}
void pop(T &data) // 消费者消费数据,输出型参数
{
sem_wait(&_dataSem); // 申请数据资源,--dataSem
pthread_mutex_lock(&_cmutex);
data = _ringQueue[_cIndex];
++_cIndex;
_cIndex %= _capacity;
cout << "消费者[" << pthread_self() << "]:消费了一个数据:" << data << endl;//便于观察
pthread_mutex_unlock(&_cmutex);
sem_post(&_roomSem); // 释放空间资源,++roomSem
}
private:
vector<T> _ringQueue; // 环形队列
uint32_t _capacity; // 环形队列大小
sem_t _roomSem; // 空间资源个数
sem_t _dataSem; // 数据资源个数
uint32_t _pIndex; // 表示生产者正在生产的位置
uint32_t _cIndex; // 表示消费者正在消费的位置
pthread_mutex_t _pmutex; // 生产者的锁
pthread_mutex_t _cmutex; // 消费者的锁
};
RingQueue.cc:
#include "RingQueue.h"
void *pRoutine(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (1)
{
int data = rand() % 1000;
rq->push(data);
}
return nullptr;
}
void *cRoutine(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (1)
{
sleep(2);
int data = -1;
rq->pop(data);
}
return nullptr;
}
int main()
{
RingQueue<int> rq(6);
pthread_t p1,p2,p3, c1,c2,c3,c4;
pthread_create(&p1, nullptr, pRoutine, &rq);
pthread_create(&p2, nullptr, pRoutine, &rq);
pthread_create(&p3, nullptr, pRoutine, &rq);
pthread_create(&c1, nullptr, cRoutine, &rq);
pthread_create(&c2, nullptr, cRoutine, &rq);
pthread_create(&c3, nullptr, cRoutine, &rq);
pthread_create(&c4, nullptr, cRoutine, &rq);
pthread_join(p1, nullptr);
pthread_join(p2, nullptr);
pthread_join(p3, nullptr);
pthread_join(c1, nullptr);
pthread_join(c2, nullptr);
pthread_join(c3, nullptr);
pthread_join(c4, nullptr);
return 0;
}
运行结果:
线程池
应用场景:
需要大量线程完成任务,且完成任务的时间较短
对性能要求苛刻的应用
接受突发性的大量请求
如何实现?
创建固定数量的线程,使用锁和条件变量保证线程同步和互斥,循环从任务队列获取任务对象,执行任务接口。
核心接口:
start:创建指定数量的线程
push:加锁操作任务队列,放入任务后,唤醒在条件下等待的一个线程
pop:直接操作任务队列,取出队头任务(线程安全在线程运行函数保证)
threadRoutine:线程运行函数,加锁判断任务队列是否为空,空则在条件变量下等待唤醒,不为空就取出任务处理,最后解锁
代码实现:
threadPool.hpp:
#include <iostream>
#include <queue>
#include <pthread.h>
#include <sys/prctl.h>
#include <assert.h>
using namespace std;
int gThreadNum = 5;
template <class T>
class ThreadPool
{
private:
ThreadPool(int threadNum = gThreadNum)
: threadNum_(threadNum), isStart_(false)
{
pthread_mutex_init(&mutex_,nullptr);
pthread_cond_init(&cond_,nullptr);
}
ThreadPool(ThreadPool<T> &) = delete;
void operator=(ThreadPool<T> &) = delete;
public:
// 线程安全地创建单例
static ThreadPool<T> *getInstance()
{
if (instance == nullptr) // 过滤掉不满足条件的
{
static pthread_mutex_t mutex_s = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex_s);
if (instance == nullptr)
{
instance = new ThreadPool<T>();
}
pthread_mutex_unlock(&mutex_s);
}
return instance;
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
prctl(PR_SET_NAME, "follower");
while(1)
{
//用条件变量保证线程间的互斥和同步
tp->lockQueue();
while(tp->queueEmpty())
{
tp->waitFortask();
}
//从任务队列中取出任务;
T t = tp->pop();
tp->unlockQueue();
cout<<"pthread["<<pthread_self()<<"]"<<"取出任务为:"<<t<<endl;
}
return nullptr;
}
void start()
{
assert(!isStart_);
for (int i = 0; i < threadNum_; i++)
{
pthread_t p;
pthread_create(&p, nullptr, threadRoutine, this); // 传入this,可以访问类内的方法
}
isStart_ = true;
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
void push(const T&in)//将任务放入任务队列中
{
lockQueue();
taskQueue_.push(in);
unlockQueue();
wakeThread();//唤醒一个线程来处理任务
}
private:
void lockQueue(){pthread_mutex_lock(&mutex_);}
void unlockQueue(){pthread_mutex_unlock(&mutex_);}
bool queueEmpty(){return taskQueue_.empty();}
void waitFortask(){pthread_cond_wait(&cond_,&mutex_);}
void wakeThread(){pthread_cond_signal(&cond_);}//依次唤醒等待队列中的线程
T pop()//从任务队列中取出任务
{
T temp = taskQueue_.front();
taskQueue_.pop();
return temp;
}
private:
bool isStart_;
int threadNum_;
queue<T> taskQueue_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
// 单例模式
static ThreadPool<T> *instance;
};
template <class T>
ThreadPool<T> * ::ThreadPool<T>::instance = nullptr;
threadPoolTest.cc:
#include "threadPool.hpp"
#include <unistd.h>
int main()
{
ThreadPool<int> *tp = ThreadPool<int>::getInstance();
tp->start();
sleep(1);
srand((unsigned int)time(nullptr));
while (1)
{
tp->push(rand() % 10000);
sleep(1);
}
return 0;
}
输出:
可以看到线程id顺序是一样的,说明了线程同步。