写一个线程池
用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 sumit(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)...)
);
这句话就是用一根智能指针指向我们的future任务。
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::unique_lock<std::mutex>lk(m_mutex);
if (m_stop) throw std::runtime_error("submit on stopped ThreadPool");
tasks.emplace([taskPtr]() {
(*taskPtr)();
});
}
这一段就是加入任务队列了,因为queue不是线程安全的,所以要加锁。这里使用了unique_lock这种锁,这种锁就是细粒度锁。
之后就很显然了,m_stop表示我们的线程池是否被停止了。emplace就是使用了移动构造函数,而push使用的是拷贝构造函数。
m_cv.notify_one();
return taskPtr->get_future();
这一段也就是使用条件变量唤醒一个线程。条件变量 std::condition_variable 是为了解决死锁而生,当互斥操作不够用而引入的。我认为条件变量就是破坏了死锁的循环等待的必要条件。
OK,我们解决了submit函数的所有语法糖,不难发现这个函数不算复杂,主要是语法糖太多。
线程池里的线程
现在的重点是线程的工作是怎么完成。
loop
if queue is not empty:
work
不断的轮询,当然这是效率不高的,它会一直询问队列是否为空。所以让我们加个锁,来让他工作的不那么频繁。
loop
if queue is not empty:
wait signal
work
这就是代码逻辑,再来看点细的:
m_thread.emplace_back(
[this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex>lk(m_mutex);
//使用条件变量,当线程池开启并且有任务时执行
m_cv.wait(lk, [this]() {
return m_stop || !tasks.empty();
});
if (m_stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}
);
终于写完了。。。。