详解C++17线程池的实现

      C++标准库在C++11之后新增了线程特性,C++的线程在此之后可以进行跨平台使用。在其他的OOP语言(例如Java、C#等)中有线程池来避免过多过频创建线程,提升了不少的性能,在网络库中也有用到线程池的情况,因此了解一个线程池的实现可以更加熟悉多线程的使用。本文将用C++17实现一个简单的线程池,github地址:GitHub - Nevermore1994/ThreadPool: A simple C++11 Thread Pool implementation

      首先介绍下类的声明

class ThreadPool
{

public:
	ThreadPool(size_t size);
	~ThreadPool();
	template<class F, typename ... Args>
	decltype(auto) Enqueue(F&&, Args&& ... args); //通过完美转发进行任务入队列
private:
	std::vector<std::thread> workers; //用来工作的线程
	std::queue<std::function<void()>> tasks; //线程池的任务队列
	std::condition_variable cond_; 
	std::mutex mutex_; 
	bool stop;
};

      ThreadPool中只有三个函数,构造函数,析构函数以及一个Enqueue(push任务队列的函数)。

      这些声明看上去平淡无奇,但看到decltype(auto)是不是有些懵?如果你不熟悉C++14可能是不会明白这是个啥东西。decltype是用来推导类型的,和auto有什么区别呢?decltype需要一个参数来推导类型,是依葫芦画瓢进行推导的,比如以下例子

int  x = 1;
int& rx = x;
decltype(x) y = 2; //x 是int
decltype(rx) ry = y; //ry 是int&

    auto的推导是比较复杂的推导,有点类似于模板的推导(除大括号初始化推导外都是相同的),会进行引用折叠(这里不再赘述,《Effective Modern C++》有详细的篇幅介绍)。decltype(auto)的意思就是用decltype的推导规则,来推导auto的型别,在这里可以获取到任务的返回值类型,在具体Enqueue函数再说。

    先介绍构造函数,代码实现如下图。

inline ThreadPool::ThreadPool(size_t size) :stop(false)
{
	for (int i = 0; i < size; i++)
	{
		workers.emplace_back([this]()
		{
			while (true) //死循环等待
			{
				std::function<void()> task;
				{   
					//临界区开始 等待线程池的销毁或者是任务的带来
					std::unique_lock<std::mutex> lock(mutex_);
					cond_.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); //条件变量的wait函数 防止虚假唤醒

					if (this->stop && this->tasks.empty())
					{
						return; //线程池销毁 跳出循环
					}

					task = std::move(this->tasks.front()); //将队列的头部移动给task
					this->tasks.pop(); //记得出队列
					
					//临界区结束 自动释放锁 mutex_
				}
				task(); //执行任务
			}
		});
	}
}

    由于是在头文件定义的构造函数实体,因此用了inline标记(不然会报重复定义错误),其实这里可以在cpp文件实现,这样就可以不用inline,想一想为什么Enqueue可以不用inline?在函数体中用whlile(true)循环等待任务的到来或者是线程池的析构(VS2013可能会有警告,可以换成for(;;))。循环内是取队列头执行。代码中已经详细注释。

    析构函数实现代码如下

inline ThreadPool::~ThreadPool()
{
	{
		//临界区开始  锁住stop 将其赋值为true 通知线程退出循环
		std::unique_lock<std::mutex> lock(mutex_);
		stop = true;
		//临界区结束 自动释放锁 mutex_
	}
	cond_.notify_all();//通知所有的线程退出循环
	for_each(workers.begin(), workers.end(), std::mem_fn(&std::thread::join)); //等待
}

   析构函数就是一些收尾工作,通知线程池里所有的线程准备析构,停止任务进队列,阻塞到所有的任务完成。

   下面就是Enqueue的实现

//原来的函数是用返回值尾序来获取类型 在C++14之后支持decltype(auto)进行类型的推导(不熟悉decltype推导规则可以看看Effective Modern C++)
template<class F, typename ...Args>
decltype(auto) ThreadPool::Enqueue(F&& f, Args && ...args)
{
	using return_type = typename std::result_of_t<F(Args...)>; //获取返回值类型

	auto task = std::make_shared< std::packaged_task<return_type()> >(
		std::bind(std::forward<F>(f), std::forward<Args>(args)...) //通过bind和完美转发将函数和参数绑定
		);

	std::future<return_type> res = task->get_future();
	
	{
		//临界区开始 锁任务队列
		std::unique_lock<std::mutex> lock(mutex_);
		
		//如果已经将线程池析构,就不允许再入队列
		if (stop)
			throw std::runtime_error("enqueue on stopped ThreadPool");

		tasks.emplace([task]() { (*task)(); });
	}
	cond_.notify_one(); //通知任意一个线程接收任务
	return res;
}

不熟悉可变模板参数,可以自行搜索相关知识。result_of_t获取到返回值类型,bind将函数和参数进行绑定,packaged_task用来包装成异步的函数,返回一个shared_ptr。这里介绍下,如果是C++11的话,那么

decltype(auto) ThreadPool::Enqueue(F&& f, Args && ...args)

应该换成如下

auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type>

或者是 

auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of_t<F(Args...)>>

前面介绍过了,这种叫返回值类别尾序语法。到这里一个简单的线程池就完成了,你可以愉快地进行多线程玩耍了。运用起来非常简单,样例在github上有,你可以去看看。

对了,前文中提到为什么Enqueue为什么可以不用inline呢?应该是成员函数是模板函数的原因,模板函数并非在定义时实例化,而在调用处实例化,这并不违反ODR原则(一处定义原则)。具体的实例化点(POI),是调用模板函数后,模板类和模板函数的实例化点并不一样,模板类在调用前,模板函数在调用后,实例化点并非真正实例化,通常真正的实例化会在翻译单元末尾处,详见《C++ template》的10.3节。(如果是错误的,希望大神能解释下)

其他:C++20出来了,Modules和Coroutines、约束新特性看起来有些意思,最近在看C++的协程,有空的也会写个协程的实现。

C++是一种面向对象的计算机程序设计语言,由美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现(最初这种语言被称作“C with Classes”带类的C)。它是一种静态数据类型检查的、支持多重编程范式的通用程序设计语言。它支持过程化程序设计、数据抽象、面向对象程序设计、泛型程序设计等多种程序设计风格。C++是C语言的继承,进一步扩充和完善了C语言,成为一种面向对象的程序设计语言。C++这个词在中国大陆的程序员圈子中通常被读做“C加加”,而西方的程序员通常读做“C plus plus”,“CPP”。 在C基础上,一九八三年又由贝尔实验室的Bjarne Strou-strup推出了C++C++进一步扩充和完善了C语言,成为一种面向 对象的程序设计语言。C++目前流行的编译器最新版本是Borland C++ 4.5,Symantec C++ 6.1,和Microsoft Visual C++ 2012。C++提出了一些更为深入的概念,它所支持的这些面向对象的概念容易将问题空间直接地映射到程序空间,为程序员提供了一种与传统结构程序设计不同的思维方式和编程方法。因而也增加了整个语言的复杂性,掌握起来有一定难度。C++由美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现(最初这种语言被称作“C with Classes”带类的C)。开始,C++是作为C语言的增强版出现的,从给C语言增加类开始,不断的增加新特性。虚函数(virtual function)、运算符重载(Operator Overloading)、多重继承(Multiple Inheritance)、模板(Template)、异常(Exception)、RTTI、命名空间(Name Space)逐渐被加入标准。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值