在计算机编程中,线程池是一种为了让程序可以并发执行的软件设计模式,也叫作 replicated worker 或者 worker-crew model(为什么都带有worker呢?我觉得是因为可以线程池并不生产任务,只是执行任务,是卑微的打工人)。线程池中维护了多个等待 task 的线程,并且有一个保存待执行 task 的 task queue(复杂的线程池实现还会有 completed taskqueue),当 task 被线程池的使用者分配并加入到 task queue 时,等待的线程会执行这些任务。通过维护线程池中的线程,该 model 可以提高程序性能并降低程序执行的延迟(因为线程池中的线程是提前创建好的,在执行任务时不需要创建线程的时间开销)。线程池中的线程数量是与计算机资源相关的,特别是 CPU 资源。
The mental model
线程池一般要包含一个 task queue 和一些实际执行任务的线程,当然有些复杂的线程池包含一个 completed task queue(用于存放任务执行的结果)。
线程池的C++实现
-
首先来分析需要用到哪些 C++ 技术:
- 既然是线程池肯定涉及到并发数据安全因此
std::mutex
、std::unique_lock
是必不可少的。 - 在把 task 放入线程池时,放的肯定是一个可调用(callable)对象和其实参。C++11 通过
std::function
和std::bind
统一了可调用对象的各种操作,因此这两者也是必不可少的。 - 有些可调用对象有返回值,有些可调用对象没有返回值,为了使我们写的线程池更具有通用性,我们需要兼容这两种情况,在C++多线程编程中,返回值一般保存在
std::future
中,那么如何获得std::future
并且和可调用对象关联起来呢?可以通过std::packaged_task
(关于用法可以参考 C++ 多线程基本操作——std::future | std::promise)。
- 既然是线程池肯定涉及到并发数据安全因此
-
其次分析线程池需要暴露的接口:
- 创建线程池的构造函数(可以指定线程池中线程数量)
- 提交计算任务的函数(可以指定可调用对象以及可调用对象运行所需要的参数)
- 线程池停止的函数
下面是 github 上一个大佬基于 C++11 实现的 ThreadPool :
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
class ThreadPool {
public:
ThreadPool(size_t);
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
~ThreadPool();
private:
// need to keep track of threads so we can join them
std::vector< std::thread > workers;
// the task queue
std::queue< std::function<void()> > tasks;
// synchronization
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
for(size_t i = 0;i<threads;++i)
workers.emplace_back(
[this]
{
for(;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
}
);
}
// add new work item to the pool
/* 在 C++14 及以后可以删除尾序返回值即(-> 和后面的内容) */
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
// don't allow enqueueing after stopping the pool
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker: workers)
worker.join();
}
#endif
附录-知识补充
typename 的作用
- typename 用于引入一个模板参数,在模板定义语法中与 class 关键字的作用完全一样。
- 用于声明一个类型(因为当使用到作用域运算符
::
操作一个模板参数的类型成员时,编译器并不知道操作的是一个类型还是一个变量。)
对于第一点无需说明,读者自然无比熟悉。但是对于第二点需要进行讲解。例如对于如下代码:
template<typename C>
void print2nd(const C& container){
if(container.size() >= 2){
C::const_iterator it(container.begin());
++it;
int value = *it;
cout<<value;
}
}
就会发生编译错误,因为编译器不知道 C::const_iterator
是个类型还是一个变量。C::const_iterator
的解析有着逻辑上的矛盾: 直到确定了C
是什么东西,编译器才会知道C::const_iterator
是不是一个类型; 然而当模板被解析时,C
还是不确定的。这时我们声明它为一个类型才能通过编译:
typename C::const_iterator it(container.begin());
std::result_of 的作用
std::result_of
是一个模板类,它可以用于推导函数调用的结果类型。它的使用方式是std::result_of::type
,其中F
是函数类型,Args...
是参数类型列表。然而,从C++17开始,std::result_of
已经被弃用,取而代之的是std::invoke_result
。在泛型编程中,std::result_of
的一个常见用途是在编写模板函数时推导函数调用的返回类型。例如,考虑以下代码:
template<typename Func, typename Arg>
auto call(Func f, Arg a) -> typename std::result_of<Func(Arg)>::type {
return f(a);
}
在这个例子中,std::result_of::type
用于推导call
函数的返回类型。这样,无论f函数的实际返回类型是什么,call
函数都能正确地推导出它。