ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

写一个线程池

2022-07-21 22:03:24  阅读:139  来源: 互联网

标签:std 函数 右值 一个 auto future 线程


用C++写一个简易线程池

什么是线程池?

用一个池子管理线程。线程过多会带来额外开销,线程池维护多个线程,等待监督管理者分配可并发的任务。一方面避免了处理任务时创建线程开销的代价,另一方面避免了线程过度膨胀导致过分调度,保证内核的充分调用。
线程池的优化思路是这样的:我们先在池子里创建若干个线程,当有事件发生时,我们再去使用这个线程,这样就可以大大减少线程的创建和销毁。

线程池的设计

需要用到这些东西。

bool m_stop;
std::vector<std::thread> m_thread;
std::queue<std::function<void()> >tasks;
std::mutex m_mutex;
std::condition_variable m_cv;
auto submit(F&& f, Args&&... args)->std::future<decltype(f(args...))> 

我们有三个重要的成分:

  • 任务队列,存储任务的地方
  • 提交函数,我们需要使用一种方法把任务提交到线程池中
  • 线程池,一组线程,需要他们来干活

任务队列\(tasks\)

使用一个数据结构存储发生的任务,你可以把它理解为生产者消费者模型中的生产者。我们希望先来的任务先处理,很自然的选择使用了队列,也许在其他的任务场景下会有更好的数据结构。

这里有一个 std::function<void()>,这是C++11引入的新玩意,function是想把一个函数当作一个对象来使用,它可以用一种统一的方式处理函数、函数对象、函数指针、lambda表达式、bind对象等等。

注意:STL容器不是线程安全的,所以在emplace或者push的时候应当加锁。

提交函数\(submit\)

template <typename F,typename... Args>
	auto submit(F&& f, Args&&... args)->std::future<decltype(f(args...))> {
		auto taskPtr = std::make_shared<std::packaged_task<decltype(f(args...))()>>(
			std::bind(std::forward<F>(f),std::forward<Args>(args)...)
			);
		{
			std::unique_lock<std::mutex>lk(m_mutex);
			if (m_stop) throw std::runtime_error("submit on stopped ThreadPool");
			tasks.emplace([taskPtr]() {
				(*taskPtr)();
				});
		}
		m_cv.notify_one();
        	return taskPtr->get_future();
	}

这一段代码就很吓人了,用到了很多C++11的新特性。

可变模板函数

typename...这就是可变模板参数,可以传入多个参数。在这里表示我们需要一个通用参数F和一个参数包Args。

函数声明
auto submit(F&& f, Args&&... args)->std::future<decltype(f(args...))>

提前说一件事

type function(args)
和
auto function(args)-> type

都是可以作为函数声明的。
在这里使用了三个东西auto,decltype,future。我们一个一个来看。
auto可以自动推导类型,所以auto变量必须得初始化的,其次auto会忽视顶层const。
decltype可以推导一个变量的类型,使用方法就是decltype(表达式),但是推导函数类型的时候我们不能写下面这种

decltype(x+y) add(T x,T y);

而应该写

add(T x,T y) -> decltype(x+y);

然后说一说std::future,这是一种特殊类型它提供了一个访问异步操作的机制。

std::future实际上是在做这样一件事:
A线程创建了一个B线程来做一件事情,但是A线程有其他事情在做,所以我们希望在某个时间节点来get()这个结果。

一个可行的方案是:
将结果放在某个全局变量中,需要的时候调用一个线程去获取这个结果。

而future则用阻塞的方式来实现:
使用packaged_task封装一个函数,紧接着使用future的get_future()来执行这个函数,而这个get就是调用了线程B,然后我们用wait()阻塞future,直到B完成这个函数。

现在我们终于看完了函数声明。

函数体
auto taskPtr = std::make_shared<std::packaged_task<decltype(f(args...))()>>(
			std::bind(std::forward<F>(f),std::forward<Args>(args)...)
			);
std::make_shared

是创建一根shared_ptr的智能指针,这个不是重点。

package_task

在前面说到过,它对函数进行封装,结果就是存在std::future对象中。抽象话就是:可以异步执行的函数的包装器。

std::bind

这是一个适配器,准确来说,这是一个函数适配器
所谓适配器,就是在一个已知的容器或者函数上进行再一次封装,比如queue,他就是一个建立在deque或者vector之上的容器适配器。而bind就是一个建议在某一种函数上的函数适配器。
它的语法是这样的:

template <class Fn, class... Args> bind (Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args> bind (Fn&& fn, Args&&... args);

一个使用的例子:

bool isBetween( int i, int min, int max) {
	return i >= min && i <= max;
}
function<bool(int)> filter = std::bind(isBetween, placeholders::_1, 20, 40);
printNumber(numbers, filter);

placeholders::_1 的意思是,这里是一个占位符,在调用的时候,将实际传递的第一个参数放到这里。
占位符的数量可以是任意多的,像这样:
std::placeholders::_1, std::placeholders::_2, …, std::placeholders::_N。
上面这个例子也表现了,std::bind的返回值就是function类。

std::forward

它有个极为抽象的名字:完美转发
std::forward()将会完整保留参数的引用类型进行转发。如果参数是左值引用(lvalue),该方法会将参数保留左值引用的形式进行转发,如果参数是右值引用(rvalue),该方法会将参数保留右值引用的形式进行转发。
事实上,我读这个东西的时候一直觉得这是useless的语法糖。
但在这个例子中是有用的:
重新看看函数声明,(F&& f, Args&&... args),这是右值引用吗?不是,这是一个很牛马的现象———万能引用

Rvalue references只能绑定到右值上,lvalue references除了可以绑定到左值上,在某些条件下还可以绑定到右值上。 这里某些条件绑定右值为:常左值引用绑定到右值,非常左值引用不可绑定到右值!

这在说什么?&&确实表示右值引用,但是左值仍然可以使用这个东西,所以我们要叫他万能引用。
而我们为了掌握引用的类型,我们需要使用一个方法——std::forward();
所以有这么一句话

在《Effective Modern C++》中建议:对于右值引用使用std::move,对于万能引用使用std::forward。

标签:std,函数,右值,一个,auto,future,线程
来源: https://www.cnblogs.com/Paranoid5/p/16503740.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有