线程池的运用场景
多线程程序是为了解决程序运行效率问题
而单线程的代码一定是串行化运行的
线程池不仅要能够提高程序运行效率,还要提高程序处理业务的种类
存在的问题:当业务种类较少的时候可以用switch case/if else列举,业务多的时候就用不了了
线程池的原理(定义)
线程池=一堆线程+线程安全队列(元素带有任务接口)
队列元素的类型=需要处理的数据+处理数据对应的函数(任务接口)
创建固定数量线程,循环从任务队列中获取任务对象(队列元素=数据+函数)
获取到任务对象后,执行任务对象中的任务接口(函数处理数据)
线程池的代码实现
线程安全的队列:线程安全=同步+互斥
元素类型:数据、处理数据的函数(任务接口)
队列:STL queue
#include
#include
#include
#include
/*
* 定义队列元素的类型
* 数据
* 处理数据的方法
* */
typedef void (*Handler)(int data);
class QueueData{
public:
QueueData(){
}
QueueData(int data, Handler handler){
data_ = data;
handler_ = handler;
}
/*
* 怎么通过函数处理数据
* */
void Run(){
handler_(data_);
}
private:
//要处理的数据
int data_;
//要处理数据的函数(要保存一个函数的地址)
//Handler 函数指针, handler 函数指针变量, 保存函数的地址
Handler handler_;
};
/*
* 线程队列
* 互斥+同步
* 元素类型
* 队列
* */
class SafeQueue{
public:
SafeQueue(){
capacity_ = 1;
pthread_mutex_init(&lock_, NULL);
pthread_cond_init(&prod_cond_, NULL);
pthread_cond_init(&cons_cond_, NULL);
}
~SafeQueue(){
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&prod_cond_);
pthread_cond_destroy(&cons_cond_);
}
void Push(QueueData& data){
pthread_mutex_lock(&lock_);
while(que_.size() >= capacity_){
pthread_cond_wait(&prod_cond_, &lock_);
}
que_.push(data);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&cons_cond_);
}
/*
* data : 出参, 返回给调用者的
* */
void Pop(QueueData* data, int flag_exit){
pthread_mutex_lock(&lock_);
while(que_.empty()){
if(flag_exit == 1){
pthread_mutex_unlock(&lock_);
pthread_exit(NULL);
}
pthread_cond_wait(&cons_cond_, &lock_);
}
*data = que_.front();
que_.pop();
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&prod_cond_);
}
void BroadcaseAllConsume(){
pthread_cond_broadcast(&cons_cond_);
}
private:
std::queue que_;
size_t capacity_;
/* 互斥锁 */
pthread_mutex_t lock_;
/*
* 同步 :
* 生产者的条件变量()
* 消费者的条件变量(线程池当中的一堆线程, 在逻辑上就是消费线程)
* */
pthread_cond_t prod_cond_;
pthread_cond_t cons_cond_;
};
class ThreadPool{
public:
ThreadPool(){
}
~ThreadPool(){
if(sq_ != NULL){
delete sq_;
}
}
int InitThreadPool(int thread_count){
/*
* 0 : 线程继续运行
* 1 : 线程退出
* */
flag_exit_ = 0;
sq_ = new SafeQueue;
if(sq_ == NULL){
printf("Init thread pool failed\n");
return -1;
}
thread_count_ = thread_count;
/*
* thread_count : 10
* */
for(int i = 0; i < thread_count_; i++){
pthread_t tid;
int ret = pthread_create(&tid, NULL, worker_start, (void*)this);
if(ret < 0){
thread_count--;
continue;
}
}
/*
* 判断一下线程的数量
* thread_count_ <= 0 : 创建工作线程全部失败, 程序就返回负数,告诉给调用者
* thread_count_ > 0 : 说明线程池初始化成功了
* */
if(thread_count_ <= 0){
printf("create thread all failed\n");
return -1;
}
//代表初始化成功
return 0;
}
/*
* 线程池的使用接口
* 只需要给使用者提供push接口,让他能够将数据push到队列当中就好
* 而 pop接口不需要提供, 因为线程池当中的线程可以自己调用到
* */
void Push(QueueData& qd){
sq_->Push(qd);
}
/* 线程入口函数 */
static void* worker_start(void* arg){
pthread_detach(pthread_self());
/*
* 1. 从队列当中拿元素
* 2. 处理元素
* */
ThreadPool* tp = (ThreadPool*)arg;
while(1){
QueueData qd;
tp->sq_->Pop(&qd, tp->flag_exit_);
qd.Run();
}
}
void thread_pool_exit(){
flag_exit_ = 1;
sq_->BroadcaseAllConsume();
}
private:
/* 线程安全的队列 */
SafeQueue* sq_;
/* 线程池当中线程的数量 */
int thread_count_;
/* 标志线程是否退出的标志位 */
int flag_exit_;
};
void Deal1(int data){
printf("i am Deal1, i deal %d\n", data);
}
void Deal2(int data){
printf("hhhhhh, i am Deal2, deal %d\n", data);
}
int main(){
/*
* 1. create thread pool
*
* 2. push data to thread pool
* */
ThreadPool tp;
int ret = tp.InitThreadPool(2);
if(ret < 0){
return 0;
}
for(int i = 0; i < 100; i++){
QueueData qd(i, Deal1);
tp.Push(qd);
}
tp.thread_pool_exit();
while(1){
sleep(1);
}
return 0;
}
线程池当中的线程应该如何退出
主线程退出了,导致线程退出。
优雅的退出方式:1)线程自己退出(收到某一种指令),而不是因为进程退出了被迫销毁
2)队列的元素没有带要处理的
读写锁的模式
以读模式加锁/以写模式加锁(读写锁使用的场景一定是大量读少量写的情况,因为读写锁允许多个线程并行的读,多个线程是互斥写的:读-写、写-写)
多个线程,访问临界资源的时候都是读临界资源的内容,不会产生二义性的结果。
读写锁的接口
初始化:int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
销毁:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁:
只读模式加锁(读模式可共享):int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
只写模式加锁(相当于互斥锁):int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
代码示例:
#include
#include
#include
pthread_rwlock_t rwl;
void* Thread_start1(void* arg){
pthread_rwlock_wrlock(&rwl);
printf("线程1 写模式加锁成功...\n");
sleep(10000);
return NULL;
}
void* Thread_start2(void* arg){
sleep(2);
pthread_rwlock_rdlock(&rwl);
printf("线程2 读模式加锁成功...\n");
sleep(10000);
return NULL;
}
int main(){
/*
* 读模式 + 读模式
* */
for(int i = 0; i < 1; i++){
pthread_t tid;
pthread_create(&tid, NULL, Thread_start1, NULL);
pthread_create(&tid, NULL, Thread_start2, NULL);
}
pthread_rwlock_init(&rwl, NULL);
//pthread_rwlock_wrlock(&rwl);
//printf("第三次写模式加锁成功...\n");
//
while(1){
sleep(1);
}
pthread_rwlock_destroy(&rwl);
return 0;
}
引用计数:用来记录当前读写锁有多少个线程以读模式获取了读写锁
当有线程以读模式进行加锁,加锁成功,则引用计数++
当以读模式打开读写锁的进程,释放了读写锁之后,引用计数--
只有当引用计数为0的时候,线程才可以以写的模式打开读写锁
如果读写锁已经以读模式打开了,有一个线程A想要以写模式打开读写锁,则需要等待,等待期间又来了新的读模式加锁的线程,读模式的线程也要跟着等待
单例类只能有一个实例
单例类必须自己创建自己的唯一实例
单例类必须给所有其他对象提供这一实例
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁的创建于销毁。
何时使用:当你想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有了这个单例,有返回,无创建。
关键代码:构造函数是私有的。
饿汉模式:在程序启动的时候就创建唯一实例对象,饿汉模式不需要加锁。
代码模拟:
#include
/*
* 饿汉模式的单例类
* */
class sigleton{
public:
static sigleton* GetInstance();
void Print(){
std::cout << "sigleton print" << std::endl;
}
private:
/* 这个静态的成员变量, 后面指向唯一的实例化对象 */
static sigleton* st;
sigleton(){};
};
/* 程序一旦启动, 就会创建全局唯一的实例化对象 */
sigleton* sigleton::st = new sigleton;
sigleton* sigleton::GetInstance(){
return st;
}
int main(){
sigleton* st = sigleton::GetInstance();
st->Print();
sigleton* st1 = new sigleton;
sigleton st2;
return 0;
}
懒汉模式:当你第一次使用是才创建唯一实例对象,从而实现延迟加载效果。懒汉模式在第一次使用单例对象是才能完成初始化工作,因此可能存在多线程竞态环境,如若不加锁会导致重复构造、构造不完全。
代码模拟:
#include
/*
* 懒汉模式的单例类
* 并不是程序启动的时候, 就实例化对象
* 而是用到的时候, 才进行实例化对象
* */
class sigleton{
public:
static sigleton* GetInstance();
void Print(){
std::cout << "sigleton print" << std::endl;
}
private:
sigleton(){};
static sigleton* st;
static pthread_mutex_t lock_;
};
pthread_mutex_t sigleton::lock_ = PTHREAD_MUTEX_INITIALIZER;
sigleton* sigleton::st = NULL;
sigleton* sigleton::GetInstance(){
if(st == NULL){
pthread_mutex_lock(&sigleton::lock_);
if(st == NULL){
st = new sigleton;
}
pthread_mutex_unlock(&sigleton::lock_);
}
return st;
}
int main(){
sigleton* st = sigleton::GetInstance();
st->Print();
std::cout << st << std::endl;
sigleton* st1 = sigleton::GetInstance();
st1->Print();
std::cout << st1 << std::endl;
return 0;
}
悲观锁:针对某个线程访问临界区修改数据的时候,都会认为可能有其他线程并行修改的情况发生,所以在线程修改数据之前就进行加锁,让多个线程互斥访问。悲观锁包括:互斥锁、读写锁、自旋锁等等。
乐观锁:针对某个线程访问临界区修改数据的时候,乐观的认为只有该线程在修改,大概率不会存在并行的情况,所以修改数据不加锁,但是在修改完毕进行更新的时候进行判断。