一.条件变量
1.1同步和互斥
我们先来了解同步和互斥,同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
互斥:指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性。
我们可以使用条件变量(condition variable)实现多个线程之间的同步操作,当条件不满足时,相关线程一直被阻塞,直到某种条件成立,这些线程才会被唤醒。
1.2条件变量的相关接口:
我们来了解相关接口:
//1.1局部初始化
int pthread_cond_init((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
//参数:
//cond:要初始化的条件变量
//attr:控制条件变量在进程内还是进程间使用,一般置为NULL,表示进程内各个线程使用。
//1.2全局初始化
pthread_cond_t my_condition = PTHREAD_COND_INITIALIZER;
//2.销毁
int pthread_cond_destroy(pthread_cond_t *cond)
//3.等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
//4.唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);全部唤醒
int pthread_cond_signal(pthread_cond_t *cond);部分唤醒
为什么 pthread_cond_wait 需要互斥量?
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须 要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件 变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有 互斥锁就无法安全的获取和修改共享数据。
pthread_cond_wait调用是:
- a. 让调用线程等待
- b. 自动释放曾经持有的_mutex锁
- c. 当条件满足,线程唤醒,pthread_cond_wait。要求线性必须重新竞争_mutex锁,竞争成功,方可返回!!!
对于条件变量,我们可以看成一个铃铛和队列,当线程对某些数据修改时,摇醒铃铛通知队列中的线程。
使用格式基本如下:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
1.3题目加深理解
我们来通过一道题目加深理解
题目描述:线程A打印-我是线程A;线程B打印-我是线程B; 最终实现交替打印,不能出现连续的相同打印。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
// 1.题目描述:线程A打印-我是线程A;线程B打印-我是线程B; 最终实现交替打印,不能出现连续的相同打印。
// 2.本题主要考察条件变量的基本使用流程
pthread_t _tid1;
pthread_t _tid2;
pthread_mutex_t mutex1=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond1=PTHREAD_COND_INITIALIZER;
pthread_cond_t cond2=PTHREAD_COND_INITIALIZER;
int cout=1;
void *threadroutine1(void *args)
{
while(1)
{
pthread_mutex_lock(&mutex1);
while(!cout)
{
pthread_cond_wait(&cond1, &mutex1);
}
std:: string s=(const char*)args;
std::cout<<"我是进程"<<s<<std::endl;
cout--;
pthread_cond_signal(&cond1);
pthread_mutex_unlock(&mutex1);
}
}
void *threadroutine2(void *args)
{
while(1)
{
pthread_mutex_lock(&mutex1);
while(cout)
{
pthread_cond_wait(&cond1, &mutex1);
}
std:: string s=(const char*)args;
std::cout<<"我是进程"<<s<<std::endl;
cout++;
pthread_cond_signal(&cond1);
pthread_mutex_unlock(&mutex1);
}
}
void CreatTwoThread()
{
pthread_create(&_tid1, nullptr, threadroutine1, (void *) ("A"));
pthread_create(&_tid2, nullptr, threadroutine2, (void *) ("B"));
std::cout<<"创建成功"<<std::endl;
}
void Wait()
{
pthread_join(_tid1, nullptr);
pthread_join(_tid2, nullptr);
}
int main()
{
CreatTwoThread();
Wait();
return 0;
}
在main函数中,先调用CreatTwoThread()函数,创建俩个线程, 让俩个线程执行threadroute1和threadroute2函数,在这俩个函数中,cout变量是绝定等待和唤醒的重要条件,当cout==1,运行线程A,cout==0运行线程B,而这种同步关系就是靠条件变量来维持的。
二.生产者消费者模型
2.1.概念
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列等容器进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列等,消费者不找生产者要数据,而是直接从阻塞队列等容器里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列等容器就是用来给生产者和消费者解耦的。
在上述模型中,仓库属于临界资源,只能由一个生产者或消费者访问。
在上述模型中我们可以总结为"321" 原则,
3种关系:
2种身份:生产者消费者
1种场所:商城(阻塞队列等容器)
2.2阻塞队列实现:
对于阻塞队列的实现,我们底层使用vector队列和条件变量来实现,文件名:BlockQueue,hpp,代码如下:
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
template <typename T>
class BlockQueue
{
private:
bool IsFull()
{
return _block_queue.size() == _cap;
}
bool IsEmpty()
{
return _block_queue.empty();
}
public:
BlockQueue(int cap) : _cap(cap)
{
_productor_wait_num = 0;
_consumer_wait_num = 0;
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_product_cond, nullptr);
pthread_cond_init(&_consum_cond, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_product_cond);
pthread_cond_destroy(&_consum_cond);
}
private:
std::queue<T> _block_queue; // 阻塞队列,是被整体使用的!!!
int _cap; // 总上限
pthread_mutex_t _mutex; // 保护_block_queue的锁
pthread_cond_t _product_cond; // 专门给生产者提供的条件变量
pthread_cond_t _consum_cond; // 专门给消费者提供的条件变量
int _productor_wait_num;//表示有几个生产者在等
int _consumer_wait_num;//表示有几个消费在等
};
在代码种我们用了俩个条件变量给生产者和消费者,一个互斥量保护队列。下面我们再来讲述给生产者和消费者使用的俩个接口:
void Enqueue(T &in) // 生产者用的接口
{
pthread_mutex_lock(&_mutex);
while(IsFull()) // 保证代码的健壮性
{
_productor_wait_num++;
pthread_cond_wait(&_product_cond, &_mutex);
_productor_wait_num--;
}
// 进行生产
_block_queue.push(in);
// 通知消费者来消费
if(_consumer_wait_num > 0)//有等待的就唤醒
pthread_cond_signal(&_consum_cond); // pthread_cond_broadcast
pthread_mutex_unlock(&_mutex);
}
void Pop(T *out) // 消费者用的接口 --- 5个消费者
{
pthread_mutex_lock(&_mutex);
while(IsEmpty()) // 保证代码的健壮性
{
_consumer_wait_num++;
pthread_cond_wait(&_consum_cond, &_mutex); // 伪唤醒
_consumer_wait_num--;
}
// 进行消费
*out = _block_queue.front();
_block_queue.pop();
// 通知生产者来生产
if(_productor_wait_num > 0)
pthread_cond_signal(&_product_cond);
pthread_mutex_unlock(&_mutex);
// pthread_cond_signal(&_product_cond);
}
当队列空时,消费者等待生产者生产后唤醒,当队列满时,生产者等待消费者生产后唤醒。要注意的我们最好写成 while(IsFull()) 来保证代码的健壮性,不要写成if,确保不会有线程唤醒后,资源不够的情况。
2.3 生产者消费者代码其他实现
1.线程的封装,为了方便我们将线程的相关操作封装成类。文件名:Thread.h
namespace ThreadModule
{
template<typename T>
using func_t = std::function<void(T&)>;
template<typename T>
class Thread
{
public:
void Excute()
{
_func(_data);
}
public:
Thread(func_t<T> func, T &data, const std::string &name="none-name")
: _func(func), _data(data), _threadname(name), _stop(true)
{}
static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!
{
Thread<T> *self = static_cast<Thread<T> *>(args);
self->Excute();
return nullptr;
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, threadroutine, this);
if(!n)
{
_stop = false;
return true;
}
else
{
return false;
}
}
void Detach()
{
if(!_stop)
{
pthread_detach(_tid);
}
}
void Join()
{
if(!_stop)
{
pthread_join(_tid, nullptr);
}
}
std::string name()
{
return _threadname;
}
void Stop()
{
_stop = true;
}
~Thread() {}
private:
pthread_t _tid;
std::string _threadname;//线程名
T &_data; // 为了让所有的线程访问同一个全局变量
func_t<T> _func;//线程要执行的函数
bool _stop;
};
}
主函数代码:
#include "BlockQueue.hpp"
#include "Thread.hpp"
#include <iostream>
#include <string>
#include <functional>
#include <string>
#include <vector>
#include <unistd.h>
#include <ctime>
using namespace ThreadModule;
int a = 10;
using Task = std::function<void()>;
using blockqueue_t = BlockQueue<Task>;
void PrintHello()
{
std::cout << "hello world" << std::endl;
}
void Consumer(blockqueue_t &bq)
{
while (true)
{
// 1. 从blockqueue取下来任务
Task t;
bq.Pop(&t);
// 2. 处理这个任务
//cout<<"消费者者处理任务"<<endl;
t(); //消费者私有
}
}
void Productor(blockqueue_t &bq)
{
int cnt = 10;
while (true)
{
sleep(1);
Task t = PrintHello;
cout<<"生产者生成任务"<<endl;
bq.Enqueue(t);
}
}
void StartComm(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq, func_t<blockqueue_t> func)
{
for (int i = 0; i < num; i++)
{
std::string name = "thread-" + std::to_string(i + 1);
threads->emplace_back(func, bq, name);
threads->back().Start();
}
}
void StartConsumer(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq)
{
StartComm(threads, num, bq, Consumer);
}
void StartProductor(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq)
{
StartComm(threads, num, bq, Productor);
}
void WaitAllThread(std::vector<Thread<blockqueue_t>> &threads)
{
for (auto &thread : threads)
{
thread.Join();
}
}
int main()
{
blockqueue_t *bq = new blockqueue_t(5);
std::vector<Thread<blockqueue_t>> threads;
// std::vector<Thread<ThreadData>> threads;
StartConsumer(&threads, 3, *bq);
StartProductor(&threads, 1, *bq);
WaitAllThread(threads);
return 0;
}