目录:
前言
打怪升级:第80天 |
---|
本文为之前讲解内容的测试实战,下方对系统调用中的线程、互斥量、条件变量以及信号量进行了适当的封装,
并且采用生产者消费者模型来进行测试,感兴趣的朋友大可一看,如有疑问,我们评论区见~。
线程 – Thread.hpp
#pragma once
#include<iostream>
#include<pthread.h>
//#include<functional>
#include<string>
using namespace std;
//typedef function<void*(void*)> func_t;
typedef void* (*func_t)(void*);
class Thread
{
public:
Thread(int num, func_t f, void *arg):_routine(f), _arg(arg)
{
_tname = "thread_" + to_string(num);
}
// 2. 为什么传this指针? -- 由于running为静态成员,没有this指针,因此为了在running函数中回调routine函数,需要传入this
void run(){ pthread_create(&_tid, nullptr, routine, this); }// ?
void join() { pthread_join(_tid, nullptr); }
string &get_tname() { return _tname; }
pthread_t &get_tid() { return _tid; }
~Thread() {}
private:
func_t get_routine() { return _routine; }
void *get_arg() { return _arg; }
// 1.为什么使用static? -- running为成员函数,加static是为了消去this指针
static void *routine(void * arg)
{
Thread *pt = static_cast<Thread *>(arg);
std::cout << "create thread: " << pt->get_tname() << std::endl;
pt->get_routine()(pt->get_arg());
}
private:
func_t _routine; // 线程入口函数
void *_arg; // 参数
string _tname; // 线程名
pthread_t _tid; // 线程id -- 地址
};
锁 – Mutex.hpp
#pragma once
#include<pthread.h>
class Mutex
{
public:
Mutex():_pmtx(new pthread_mutex_t){}
void init(){pthread_mutex_init(_pmtx, nullptr);}
void destroy(){pthread_mutex_destroy(_pmtx);}
void lock() {pthread_mutex_lock(_pmtx);}
void unlock(){pthread_mutex_unlock(_pmtx);}
pthread_mutex_t * get_ptr_mutex() {return _pmtx;}
~Mutex(){ delete _pmtx;}
private:
pthread_mutex_t *_pmtx; // 封装的锁的指针
};
条件变量 – Condition.hpp
#pragma once
#include<pthread.h>
#include"Mutex.hpp"
class Condition
{
public:
Condition():_pcond(new pthread_cond_t){}
void init(){pthread_cond_init(_pcond, nullptr);}
void destroy(){pthread_cond_destroy(_pcond);}
void wait(Mutex& mtx){pthread_cond_wait(_pcond, mtx.get_ptr_mutex());};
void signal(){pthread_cond_signal(_pcond);}
void allsignal(){pthread_cond_broadcast(_pcond);}
~Condition(){ delete _pcond;}
private:
pthread_cond_t *_pcond; // 封装条件变量指针
};
实例1:阻塞队列 – BlockQueue.hpp
#include<queue>
#include"Mutex.hpp"
#include"Condition.hpp"
const int N = 5; // 默认队列大小
template<class T>
class blockqueue
{
public:
blockqueue(int cnt = N):_cap(cnt)
{
_mtx.init();
_consumer_cond.init();
_producer_cond.init();
}
void push(const T &val)
{
_mtx.lock();
//if(_q.size() == _cap) // ??
// 多生产多消费情况 -- 多个生产者都在条件变量上阻塞,如果使用广播唤醒,且此时只有一个空位
// 则多个生产者会同时去竞争锁,之后醒来,如果多个生产者轮次获得了锁,此时因为已经走过了判断容量的过程,因此之后不会再次判断,
// 直接往后进 --- 开始放入产品,就会超出阻塞队列容量,因此唤醒之后需要再次判断是否有空位。
while(_q.size() == _cap)
{
_producer_cond.wait(_mtx);
}
_q.push(val);
// 互相唤醒 -- 放入一个,我能保证现在至少有一个产品,叫醒消费者来消费
// 可以加条件判断 -- 生产产品数大于容量的一半了再唤醒消费者
//if(_q.size() > _cap / 2)
_consumer_cond.signal();
_mtx.unlock();
}
void pop(T &val)
{
_mtx.lock();
while(_q.empty())
{
_consumer_cond.wait(_mtx);
}
val = _q.front();
_q.pop();
//if(_q.size() < _cap / 2)
_producer_cond.signal();
_mtx.unlock();
}
~blockqueue()
{
_mtx.destroy();
_consumer_cond.destroy();
_producer_cond.destroy();
}
private:
queue<T> _q; // 模拟阻塞队列
int _cap; // 限制队列容量
Mutex _mtx; // 对阻塞队列的互斥访问
Condition _consumer_cond; // 消费者取出条件
Condition _producer_cond; // 生产者放入条件
};
运行测试
多生产多消费模型,限制阻塞队列容量
两种场景:
-
- 生产者速度快
-
- 消费者速度快
#include<iostream>
#include<unistd.h>
#include"Thread.hpp"
#include"Blockqueue.hpp"
#include<string>
using namespace std;
// 封装一个线程库,封装传参、执行、等待等库接口
// 模拟实现 -- 数据队列 -- cp模型
void *consumer_routine(void *arg)
{
blockqueue<int> *bq = static_cast<blockqueue<int> *>(arg);
while(true)
{
sleep(1); // 消费减速
int val;
bq->pop(val);
cout << "consumer pop: " << val << endl;
}
}
void *producer_routine(void *arg)
{
blockqueue<int> *bq = static_cast<blockqueue<int> *>(arg);
while(true)
{
// sleep(1); // 生产减速
int val = rand() % 100;
bq->push(val);
cout << "producer push: " << val << endl;
}
}
int main()
{
srand(time(0));
blockqueue<int> bq;
pthread_t cons[2], prod[3]; // 多生产多消费
for(int i=0; i<2; ++i)
pthread_create(cons + i, nullptr, consumer_routine, (void *)&bq);
for(int i=0; i<3; ++i)
pthread_create(prod + i, nullptr, producer_routine, (void *)&bq);
for(int i=0; i<2; ++i)
pthread_join(cons[i], nullptr);
for(int i=0; i<3; ++i)
pthread_join(prod[i], nullptr);
cout << "all thread exit" << endl;
return 0;
}
1.生产者速度快
现象:瞬间放满阻塞队列,之后消费一个生产一个。
2.消费者速度快
实验现象:生产一个消费一个。
信号量 – Semaphore.hpp
#include<semaphore.h>
class Semaphore
{
public:
Semaphore(int pORt, int num):_psem(new sem_t) { sem_init(_psem, pORt, num); }
void P() { sem_wait(_psem); }
void V() { sem_post(_psem); }
~Semaphore() { sem_destroy(_psem); }
private:
sem_t *_psem;
};
任务 – Task.hpp
#include<string> // to_string
class Task // 五则运算
{
public:
Task() {}
Task(int a, int b, char op):_a(a), _b(b), _op(op) {}
void execute()
{
switch(_op)
{
case '+': _result = _a + _b; break;
case '-': _result = _a - _b; break;
case '*': _result = _a * _b; break;
case '/':
{
if(_b == 0) throw("is division zero"); // 抛异常
else _result = _a / _b;
break;
}
case '%':
{
if(_b == 0) throw("is mod zero");
else _result = _a % _b;
break;
}
}
}
string get_exp() // 公式
{
return std::to_string(_a) + _op + std::to_string(_b) + " = ";
}
int get_res() // 结果
{
return _result;
}
~Task(){}
private:
int _a;
int _b;
char _op;
int _result;
};
实例2:环形队列 – RingQueue.hpp
#pragma once
#include<vector>
#include"Mutex.hpp"
#include"Semaphore.hpp"
const int N = 5;
template<class T>
class ringqueue
{
public:
ringqueue(int cap = N):_v(cap), _cap(cap),_c_sem(0, 0), _p_sem(0, cap){}
void push(const T& val)
{
// 下标重叠位置:
// 1.最开始环形队列中资源为0个,一定是生产者先运行,消费者阻塞在条件变量上
// 2.队列数据已满,一定是消费者先运行,生产者阻塞在条件变量上,因此无需手动控制
_p_sem.P();
_p_mtx.lock();
_v[_p_sub++] = val;
_p_sub %= _cap;
_p_mtx.unlock();
_c_sem.V();
}
void pop(T& val)
{
_c_sem.P();
_c_mtx.lock();
val = _v[_c_sub++];
_c_sub %= _cap;
_c_mtx.unlock();
_p_sem.V();
}
~ringqueue(){}
private:
vector<T> _v;
int _cap;
Mutex _c_mtx; // 互斥访问队列 -- 多个消费者同时访问一块资源,需要互斥访问
Mutex _p_mtx; // 互斥访问队列 -- 多个生产者同时访问一块资源,需要互斥访问
int _c_sub = 0; // 准备读取的位置
int _p_sub = 0; // 准备写入的位置
Semaphore _c_sem; // 消费信号量,记录可以消费的数据个数
Semaphore _p_sem; // 生产信号量,记录可以生产的数据个数
};
环形队列与阻塞队列的区别: 作为整体的目标不同;
阻塞队列把整个队列作为一个整体: 生产者消费者共用一把锁, 同一时间只允许进行一种操作,不伦是生产还是消费.
环形队列将数组中的每一个元素作为一个整体: 生产者和消费者各有各的锁, 除非是访问同一块区域(此时会通过信号量来阻塞一种操作), 其他情况下, 因为访问的是不同的整体, 生产和消费可以同时进行.
运行测试
单生产单消费模型,限制阻塞队列容量
两种场景:
-
- 生产者速度快
-
- 消费者速度快
(也满足多生产多消费)
- 消费者速度快
#include<iostream>
#include<unistd.h>
#include<cstring>
#include"Thread.hpp"
#include"Ringqueue.hpp"
#include"Task.hpp"
using namespace std;
// 模拟实现 -- 环形队列 -- 信号量 -- cp模型
void *consumer_routine(void *arg)
{
ringqueue<Task> *rq = static_cast<ringqueue<Task> *>(arg);
while(true)
{
sleep(1);
Task t;
rq->pop(t);
try
{
t.runtask();
cout << "consumer pop: " << t.get_exp() << t.get_res() << endl;
}
catch(const char *s)
{
cerr <<"exception: " << s << endl;
}
}
}
void *producer_routine(void *arg)
{
ringqueue<Task> *rq = static_cast<ringqueue<Task> *>(arg);
while(true)
{
char opArr[] = "+-*/%";
int a = rand() % 5;
int b = rand() % 5;
int opi = rand() % strlen(opArr);
Task t(a, b, opArr[opi]);
rq->push(t);
cout << "producer push: " << t.get_exp() + '?' << endl;
}
}
int main()
{
srand(time(0));
ringqueue<Task> rq;
thread t1(consumer_routine, (void *)&rq, "consumer thread");
thread t2(producer_routine, (void *)&rq, "producer thread");
t1.run();
t2.run();
t1.join();
t2.join();
cout << "all thread exit" << endl;
return 0;
}
1.生产者速度快
现象:瞬间放满阻塞队列,之后消费一个生产一个。
2.消费者速度快
实验现象:生产一个消费一个。
实例3:线程池 – ThreadPool.hpp
#include<vector>
#include<queue>
#include<iostream>
#include"Thread.hpp"
const static int N = 5;
template<class T>
class ThreadPool
{
private: // 简单封装
void lock() { pthread_mutex_lock(&_mtx); }
void unlock() { pthread_mutex_unlock(&_mtx); }
void wait() { pthread_cond_wait(&_cond, &_mtx); }
void signal() { pthread_cond_signal(&_cond); }
bool isEmpty() { return _tasks.empty(); }
static void * routine(void *arg) // 3. 线程们会在这里等待任务的到来, 如果没有任务就会阻塞
{
ThreadPool<T> * tp = static_cast<ThreadPool<T> *>(arg);
while(true)
{
tp->lock();
while(tp->isEmpty()) tp->wait();
T t;
tp->taskPop(t);
tp->unlock();
try
{
t.execute();
}
catch(const char *s)
{
std::cerr << s << endl;
}
cout << t.get_exp() << t.get_res() << endl;
}
}
public:
ThreadPool(int num = N) :_num(num)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
void init() // 1. 线程们初始化, 使用需要的数据初始化Thread对象
{
for(int i=0; i<_num; ++i)
{
_threads.push_back(Thread(i, routine, this));
}
cout << "all thread init" << endl;
}
void start() // 2. 启动线程, 线程们会进入routine函数
{
for(int i=0; i<_num; ++i)
{
_threads[i].run();
}
cout << "all thread start" << endl;
}
void taskPush(const T t)
{
lock();
_tasks.push(t);
unlock();
signal(); // 发排任务,唤醒执行
}
void taskPop(T &t)
{
// lock(); **加锁的情况下才能够进来取数据,因此此时不需要加锁,否则死锁**
t = _tasks.front();
_tasks.pop();
// unlock();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
private:
vector<Thread> _threads; // 线程数组
int _num; // 线程个数 -- 虽然vector本身就可以获取这个数据, 不过加上方便使用
queue<T> _tasks; // 任务队列
pthread_mutex_t _mtx; // 互斥访问任务队列
pthread_cond_t _cond; // 无任务,线程阻塞
};
运行测试
单生产多消费,
由于没有像上方一样现在任务队列容量,因此不方便观察“生产者速度快”的实验现象,
此处只演示消费者速度快。
#include<iostream>
#include<memory> // unique_ptr
#include<cstring> // strlen
#include<unistd.h> // sleep
using namespace std;
#include"Task.hpp"
#include"ThreadPool_v2.hpp"
int main()
{
unique_ptr<ThreadPool<Task> > tp(new ThreadPool<Task>());
tp->init();
tp->start();
const char *ops = "+-*/%";
while(true) // 生产者生产任务
{
int a = rand() % 100;
int b = rand() % 100;
int opi = rand() % strlen(ops);
tp->taskPush(Task(a, b, ops[opi]));
cout << "push task..." << endl;
sleep(1); // 生产者速度慢
}
return 0;
}
> 实验现象:生产一个消费一个。
总结
使用条件变量若进行等待,会自动释放锁,且在被唤醒之后会再次申请锁.
因此pthread_cond_wait的第二个参数为 锁的指针,当该条件变量上的进程被唤醒时会自动去申请锁,只有当再次申请到锁时才会继续往后运行,否则会重新在锁上阻塞。
何时使用锁,何时使用信号量:
一份资源当做整体去访问时要加锁,上方的阻塞队列;
一份资源分多块访问时使用信号量,表示可访问资源,或资源分块数量,上方的环形队列。