基于c++11,14,17新特性的线程池

项目简介

项目地址:
https://gitee.com/cai-jinxiang/thread_pool
使用的c++新特性:

  • 简单实现 c++ 17 Any,semaphore

  • c++ 14 函数返回值类型推导

  • c++ 11 package_task,future,decltype,lambda表达式,可变参模板,智能指针,thread,函数模板类型推导,引用折叠(package_task的左值引用的拷贝和赋值运算符都被删除了)

  • 支持fixed模式和cached模式(任务提交不上时创建新线程,定时清除长期闲置的线程)

  • 基于可变参模板和引用折叠,实现submitTask接口,支持任意任务函数及任意数量参数

  • 使用future类型异步返回任务结果

  • 使用map,queue管理线程对象和任务

  • 基于条件变量condition_variable和互斥锁mutex实现任务提交线程和任务执行线程间的通信机制

多线程的优势

多线程程序一定就好吗?不一定,要看具体的应用场景:

IO密集型

程序的指令,涉及IO操作,比如设备,文件,网络操作(等待客户端连接,程序会被阻塞住,若cpu给这样的线程分配时间片,cpu属于空闲下来的)无论是CPU单核、CPU多核、多CPU,都是比较适合多线程程序的

CPU密集型

程序的指令多是在做计算

  • CPU单核
    多线程存在上下文切换,是额外的花销,线程越多上下文切换所花费的额外时间也越多,倒不如一个线程一直进行计算。
  • CPU多核、多CPU
    多个线程可以并行执行,对CPU利用率好。

线程数量

  • 线程的创建和销毁是非常‘重’的操作
  • 线程栈本身占用空间大,线程有自己的栈区,每个线程的用户空间都是共享的,
  • 线程调度需要上下文切换,切换花费的cpu时间多
  • 大量线程从阻塞队列被唤醒,加入就绪队列,导致系统出现锯齿状负载或者瞬时负载量很大,导致宕机

线程池的优势
操作系统上创建线程和销毁线程都是很"重"的操作,耗时耗性能都比较多,那么在服务执行的过程中,如果业务量比较大,实时的去创建线程、执行业务、业务完成后销毁线程,那么会导致系统的实时性能降低,业务的处理能力也会降低。线程池的优势就是(每个池都有自己的优势),在服务进程启动之初,就事先创建好线程池里面的线程,当业务流量到来时需要分配线程,直接从线程池中获取一个空闲线程执行task任务即可,task执行完成后,也不用释放线程,而是把线程归还到线程池中继续给后续的task提供服务。

fixed模式线程池

线程池里面的线程个数是固定不变的,一般是ThreadPool创建时根据当前机器的CPU核心数量进行指定。

cached模式线程池

线程池里面的线程个数是可动态增长的,根据任务的数量动态的增加线程的数量,但是会设置一个线程数量的阈值(线程过多的坏处上面已经讲过了),任务处理完成,如果动态增长的线程空闲了60s还没有处理其它任务,那么关闭线程,保持池中最初数量的线程即可。

在这里插入图片描述

线程同步

  • 竞态条件(race condition)指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。在多线程情况下,存在竞态条件,这段代码是不可重入的,称为临界区代码,需要保证原子操作。不存在竞态条件,那么可重入

线程互斥

  • 互斥锁
  • atomic原子类型

线程通信

  • 条件变量condition_variable(使用时需要搭配互斥锁mutex)
//生产者消费者模型
mutex mtx;
condition_variable cond;

//生产者访问临界区
unique_lock<mutex> lock(mtx);//不使用lock_guard的原因是,lock_guard仅仅是构造加锁,析构解锁,无其他接口,信号量需要对锁进行释放
while(full()){
	cond.wait(lock);
}
//生产完后,通知消费者

produce();
cond.notify_all();

//消费者访问临界区
unique_lock<mutex> lock(mtx);
while(empty()){
	cond.wait(lock);
}
cast();
cond.notify_all();
  • 信号量semaphore(线程通信)
    看作资源计数没有限制的mutex互斥锁,mutex的计数为1,加锁为0,没人加锁为1.
counting_semaphore sem(0);//资源初始为0

//生产者
procduec();
sem.release();//资源+1

//消费者
cast();
sem.aquare();//资源-1

项目梳理

  • 封装线程池成一个库,提供fixed,cached模式,传入任务,返回结果
  • 线程池包括一个线程队列,存储线程,线程数量可以扩容
  • 一个任务队列需要考虑线程安全,线程不断访问,完成任务,存储一个抽象基类Task指针,当用户想使用此线程池库时,让任务继承Task基类并重写run方法即可,
    在这里插入图片描述
  • ThreadPool类,包含一个Thread的队列,一个Task队列,一个线程函数,函数内容是循环去任务队列中消费线程,创建线程类的时候,把线程函数传给线程类
class ThreadPool {
public:
	ThreadPool();
	~ThreadPool();

	//设置模式
	void setMode(PoolMode mode);


	//设置任务上限
	void setTaskQueMaxThresHold(int thresHold);

	//传入一个智能指针,防止用户传入一个临时对象
	void submitTask(shared_ptr<Task> sp);

	void start(int initThreadSize = 4);


	ThreadPool(const ThreadPool&) = delete;
	ThreadPool& operator=(const ThreadPool&) = delete;

private:
	//定义线程函数,传给线程类,让线程类创建线程时传入即可
	void threadFunc();

	vector<std::unique_ptr<Thread>> threads_;//线程列表
	size_t initThreadSize_; //初始线程数量

	//考虑如果用户传入一个临时对象,此处不能用普通指针
	queue<shared_ptr<Task>> taskQue_;//任务队列
	atomic_uint taskSize_;//任务数量
	int taskQueMaxThesHold_;//任务数量上限

	mutex taskQueMtx_;//保证任务队列线程安全
	condition_variable notFull_, notEmpty_;//表示任务队列不空,不满的条件变量

	PoolMode poolMode_;
};
  • Thread类,构造函数接收一个函数对象,在start()函数中创建线程去运行函数对象,
//线程类型
class Thread {
public:
	//线程函数对象类型
	using ThreadFunc = function<void()>;

	//线程构造
	Thread(ThreadFunc func);
	// 线程析构
	~Thread();
	//启动线程
	void start();

private:
	ThreadFunc func_;
};
  • Task基类,用户想用这个线程库时,将任务继承Task类,重载run函数,传入线程池即可
//任务抽象基类
class Task {
public:
	virtual void run() = 0;
};
  • 使用方式

ThreadPool pool;
//pool.setMode();
pool.start(4);
class MyTask: public Task{
public:
	void run(){ }
};

Result.result = pool.submitTask(std::make_shared<MyTask>());
//使用result.get().Cast<结果类型>(),使用c++17的any获取到任务结果

Any类型的实现

  • 当用户想要获取到线程执行的结果时,线程池需要返回一个通用类,但是任务Task是一个基类,run函数是虚函数,虚函数无法写成模板函数,模板函数实例化前不会有地址,但是虚函数的虚函数表,虚函数表需要指向函数地址。
  • 因此Task类的run 需要返回一个任意类型,c++17的Any类的实现,返回值为Any,接收的时候,编译器会查看有没有一个合适的构造函数去构造Any
  • 注意模板类的声明和定义都在.h文件中
//Any类型
class Any {
public:
	Any() = default;
	~Any() = default;
	//成员是unique_ptr,其把左值引用的拷贝构造删除了
	Any(const Any&) = delete;
	Any& operator=(const Any&) = delete;

	Any(Any&&) = default;
	Any& operator=(const Any&&) = default;

	//这个构造函数可以接收任意其他数据
	template<typename T>
	Any(T data) :base_(std::make_unique<Derive<T>>(data)) {}

	//这个函数来提取基类指针指向派生类对象的那个模板成员
	template<typename T>
	T cast_() {
		//将自己的基类指针转为派生类指针,通常情况下只能派生类指针转基类指针,但是当基类指针确实是指向派生类对象时,可以转
		//dynamic_cast有RTTI识别,能判断基类指针是否指向派生类对象,来决定能否转.

		Derive<T>* pd = dynamic_cast<Derive<T>*>(base_.get());
		if (pd == nullptr) {
			//当用户的Task.run()函数返回值和,调用cast_()的模板参数不相关时,转型失败,
			//例如run返回int,但是使用cast_<long>()接收,此时上一行所作的是下面,但是base_指向的是Derive<int>
			//dynamic_cast<Derive<int>*>(base_.get());
			throw "type is unmatch!";
		}

		return pd->data_();
	}
private:
	//基类类型
	class Base {
	public:
		virtual ~Base() = default;
	private:
	};

	//派生类类型
	template<typename T>
	class Derive : public Base {
	public:
		Derive(T data) :data_(data) {}
	//private:
		T data_;
	};

	//定义一个基类指针
	std::unique_ptr<Base> base_;
};

Any类未实现的地方:

  • 清除值
  • 返回值类型
  • 判断值是否有效(放在了resulte类中判断)

信号量的实现

 Result.result = pool.submitTask(std::make_shared<MyTask>());
 int sum = res.get().cast_<int>();
  • 为了实现上述的代码,显然提交任务和执行任务不是一个线程,当用户需要通过submitTask获取到结果时,主线程需要等待执行任务的线程通知结果,c++20实现了semaphore
class Semaphore {
public:
	Semaphore(int limit = 0):resLimit_(limit){}
	~Semaphore() = default;

	//获取一个信号量资源
	void wait() {
		std::unique_lock<mutex> lock(mtx_);
		//等待信号量
		cond_.wait(lock, [&]()->bool {return resLimit_ > 0; });
		resLimit_--;
	}

	//增加一个信号量资源
	void post() {
		std::unique_lock<mutex> lock(mtx_);
		resLimit_++;
		cond_.notify_all();
	}
private:
	int resLimit_;//资源计数器
	mutex mtx_;
	condition_variable cond_;
};

与c++ 20的信号量相比:
c++20的信号量实现了两个版本,
① counting_semaphore 实现非负资源计数的信号量,底层采用的CAS原子变量
② binary_semaphore 仅拥有二个状态的信号量

任务类和结果类互相绑定

 Result.result = pool.submitTask(std::make_shared<MyTask>()); //提交任务
 int sum = res.get().cast_<int>();	//获取结果,阻塞等待线程完成任务

//Result中的代码
Any Result::get() {
	if (!isVaild_) { //构造函数初始化isVaild_,如果提交成功返回的是Result(sp, true)否则返回Result(sp, false)
		return "";
	}
	sem_.wait();
	return std::move(any_);
}

void Result::setVal(Any any) {
	//存储task
	this->any_ = std::move(any);
	sem_.post();

}

需要实现以上功能

  1. 任务执行完成之后,返回值交给Result,但是提交任务是在一个线程,完成任务是另一个线程
  • 通过信号量类,两个线程进行通信
  1. 一个任务类和一个结果类应该互相绑定,并在创建result类的时候,完成绑定。
Result::Result(std::shared_ptr<Task> task, bool isVaild = true)
	:isVaild_(isVaild), task_(task) {
	task_->setResult(this);
}

void Task::setResult(Result* res) {
	result_ = res;
}
class Task {
public:
	Task();
	~Task() = default;
	virtual Any run() = 0;

	void setResult(Result* res);
	void exec();

private:
	Result* result_;

};


Task::Task():result_(nullptr){}

void Task::exec() {
	if (result_ != nullptr) {
		result_->setVal(run());
	}
}

void Task::setResult(Result* res) {
	result_ = res;
}


//Result
//实现接收提交到线程池的task任务执行完成后的返回值类型Result
class Result {
public:

	Result(std::shared_ptr<Task> task, bool isVaild);
	~Result() = default;

	//问题一:task_执行结束之后需要赋值给any_,什么时候做?
	void setVal(Any any);
	
	//问题二:get()方法,用户调这个方法获取返回值,需要考虑阻塞
	Any get();

private:
	Any any_;
	Semaphore sem_;//线程通信信号量
	std::shared_ptr<Task> task_; //指向对应获取任务的返回值的任务对象
	std::atomic_bool isVaild_;//任务是否提交完成
};


Result::Result(std::shared_ptr<Task> task, bool isVaild = true)
	:isVaild_(isVaild), task_(task) {
	task_->setResult(this);
}

Any Result::get() {
	
	if (!isVaild_) {
		return "";
	}
	
	sem_.wait();
	return std::move(any_);
}

void Result::setVal(Any any) {
	//存储task
	this->any_ = std::move(any);
	sem_.post();
}

线程池流程梳理

ThreadPool pool;
		pool.start(4);
		Result res1 = pool.submitTask(std::make_shared<MyTask>(1, 100000000));
		//pool.submitTask(std::make_shared<MyTask>(1, 100000000));
		//pool.submitTask(std::make_shared<MyTask>(1, 100000000));
		ULong sum1 = res1.get().cast_<ULong>();
		cout << "sum1 = " << sum1 << endl;
  • 使用者将自己的任务封装并继承Task类,重写task的run方法
  • 创建线程池
    • 线程池会自动根据模式创建对应数量的线程类(包含一个线程的指针,和函数,调用start方法时,创建线程并传入函数)
    • 线程创建完成后会循环去任务队列中获取任务,如果是cached模式,会超时之后回收线程
  • 用户传入封装好的任务,返回result类
    • 如果是 cached,且任务队列中任务多 会向池中加入新线程
  • 用户通过submitTask函数向池中传入一个任务的智能指针,智能指针保证即使Task类出作用域,也不会被析构
  • 线程接拿到任务后,将任务从队列中删除(线程的函数是在线程中定义的成员函数,传入时绑定了this指针,因此可以访问到任务队列,并进行修改)
  • 线程执行后,Task类中存在一个结果类的成员,会将执行完的结果存入结果类中

遇到的线程回收死锁问题

在这里插入图片描述
线程回收的过程:
(cache模式下,Thread类中放了一个lasttime,每次执行完任务都会重置一次,当抢到锁,但发现任务队列空,且是cache模式,线程数量又比设置的大,就与lasttime对比超过60s就回收)

  1. 线程池结束,修改isPoolRunning,通知所有线程,
    1. 当线程池处在①(任务队列为空,等待通知)和②(执行任务,执行任务完成后发现isPoolRunning被修改,此线程直接回收)位置时,都能成功回收,
    2. 当线程池处在③位置时,不论线程池先取锁还是线程先取锁,都缺少了线程池notify_all的那一步,造成线程一直在等待通知阻塞在①
      解决方案(对应图片左下角的1和2):
  2. 锁+双重判断,判断isPoolRunning-》拿锁-》再判断一次isPoolRunning(没有这次判断的话,又进入循环,发现任务队列为空,等待通知,但是此时线程池不会再通知了)
  3. notify_all移动到取锁之后,当处在③的线程先取到锁,进入循环,此时线程池才完成isPoolRunning的修改,线程会等待在②位置等待通知,此时线程池拿到锁还能进行一次通知,解决思索问题

编译为.so动态库

  • 编译器会去/usr/lib /usr/local/lib 找.a .so动态库 去/usr/include /usr/local/include 找.h
  1. 库名前加上lib,后面加上.so
    在这里插入图片描述
  2. 将编译好的动态库放到/usr/lib /usr/local/lib中, 将头文件放到/usr/include /usr/local/include
  3. 编译测试文件-l加动态库名称,去掉前缀的lib和后缀的.so
    在这里插入图片描述
  4. 运行文件报错,原因是运行时找不到动态库
    在这里插入图片描述
  5. 在配置文件 /etc/ld/ld.so.cache中保存配置文件,做如下操作,在ld.so.conf.d里面新建文件,写入动态库的路径,然后运行ldconfig,相当于刷新一下配置,就可以运行了
    在这里插入图片描述
    在这里插入图片描述

利用c++ 14,17修改线程池

package_task

与function一样是一个函数对象,支持get_future()方法,获取future的返回值,运行结束后future.get()来取得结果
这与submittask返回result异曲同工
使用方式:

int sum(int a, int b) {
    return a + b;
}

int main() {
    packaged_task<int(int, int)> task(sum);
    future<int> ret = task.get_future();

    thread t1(move(task), 10, 20);//packaged_task的左值引用的拷贝构造被删除了
 
    t1.detach();
    cout << ret.get() << endl;//阻塞等待task完成
   
    return 0;
}

使用此方式代替Task和result

重写submitTask,引用折叠(package_task只能右值引用拷贝)和可变参模板
使用auto 进行返回值的类型推导,返回值类型需要写在函数名后面,写前面编译器不认识func

template<typename Func, typename... Args>
auto submitTask(Func&& func, Args&&... args) -> std::future<decltype(func(args...))>
{
}

任务队列

// 线程函数对象类型,不同的任务返回值不同,因此使用中间层进行封装,将任务封装为一个返回void的对象,返回值通过package_task.getfuture获取,线程类中的线程函数的定义:
using ThreadFunc = std::function<void(int)>;

//线程池类中Task的定义,将这个放入线程池
using Task = std::function<void()>;
std::queue<Task> taskQue_; // 任务队列

//提交任务 加入线程池 简化代码 注意如何提交一个通用类型的task
template<typename Func, typename... Args>
	auto submitTask(Func&& func, Args&&... args) -> std::future<decltype(func(args...))>
	{
		// 打包任务,放入任务队列里面
		using RType = decltype(func(args...));
			//1.任务被打包为一个packaged_task对象 一个函数对象的智能指针,一个返回值为RType,参数为空的函数指针,其实参数不为空,参数是被bind绑定了
		auto task = std::make_shared<std::packaged_task<RType()>>(
			std::bind(std::forward<Func>(func), std::forward<Args>(args)...));

		std::future<RType> result = task->get_future();

		// 获取锁
		std::unique_lock<std::mutex> lock(taskQueMtx_);
		// 用户提交任务,最长不能阻塞超过1s,否则判断提交任务失败,返回
		if (!notFull_.wait_for(lock, std::chrono::seconds(1),
			[&]()->bool { return taskQue_.size() < (size_t)taskQueMaxThreshHold_; }))
		{
			// 表示notFull_等待1s种,条件依然没有满足
			//任务提交失败,返回一个空任务的返回值
			std::cerr << "task queue is full, submit task fail." << std::endl;
			auto task = std::make_shared<std::packaged_task<RType()>>(
				[]()->RType { return RType(); });
			(*task)();
			return task->get_future();
		}

		// 如果有空余,把任务放入任务队列中
		// taskQue_.emplace(sp);  
		// using Task = std::function<void()>;
		//任务队列中是返回void的函数对象(因为不知道具体返回值)而此时提交的不是void,创建一个返回void的中间层,将Task包进去,在返回void的匿名函数里面直接调用(*task)()即可
		taskQue_.emplace([task]() {(*task)(); });
		taskSize_++;

		// 因为新放了任务,任务队列肯定不空了,在notEmpty_上进行通知,赶快分配线程执行任务
		notEmpty_.notify_all();
		

		// 返回任务的Result对象
		return result;
	}


线程池的收获

  • Any类,Semaphore的实现,不足
  • 死锁问题的解决
  • 在linux上遇到死锁,问题在任务提交上去之后,返回的Resulte类马上析构了,但是线程最终都死锁在了cond_.wait上。
    分析原因:
    1. 自己实现的semaphore使用了锁和条件变量,使用的析构函数是默认析构,调用的也是成员变量的默认析构,
    2. 在vs下锁和条件变量都会释放资源,当线程执行完Task,填写resulte的时候,那些资源都被释放了,什么都不会发生。
    3. 但是linux的g++里面锁和条件变量的默认构造也什么都没有做,因此都阻塞在了cond_.wait上,在vs上cond_已经被析构了,不会再执行了。
  • 在v1.0版本中,用户需要继承Task类,将Task类传入线程池,如果传入的Tsak是普通指针,那么客户如果析构,线程池中的Task也会消失,任务提交完成了但是却不会执行。因此需要提交智能指针。
  • v2.0通过packaged_task,future,可变参模板,将1.0的Task剔除,用户只需要提供函数和参数就可以
    实现方式:
template<typename Func, typename... Args>
	auto submitTask(Func&& func, Args&&... args) // -> std::future<decltype(func(args...))> //c++14开始auto可以进行函数返回值推导
	{
		// 打包任务,放入任务队列里面
		using RType = decltype(func(args...));
		//1.任务被打包为一个packaged_task对象 一个函数对象的智能指针,一个返回值为RType,参数为空的函数指针,其实参数不为空,参数是被bind绑定了
		auto task = std::make_shared<std::packaged_task<RType()>>(
			//std::bind(std::forward<Func>(func), std::forward<Args>(args)...));
			std::bind(func, std::forward<Args>(args)...)); //放入线程池任务队列中的是绑定好参数的函数指针

		std::future<RType> result = task->get_future();
		//将任务加入队列...
		//当任务队列满的情况,进行增加线程(如果cached模式)
		//返回future
	}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值