为什么需要线程池
首先我们要知道池(pool)
的概念,池是一组资源的集合,这组资源在一开始先被创建并初始化,当客户需要相关的资源,就可以直接从池中获取,无需动态分配。这样取得资源的速度要比动态分配快得多。当处理完一个客户连接后,可以把相关的资源放回池中,无须手动释放资源。根据不同的资源类型,池可分为多种,常见的有 内存池
、进程池
、线程池
和 连接池
。
当我们需要一个工作线程来处理客户请求时,我们可以支持从线程池中取得一个执行实体,而无需手动穿件线程
简单线程池的实现
对于简单的线程池来说,客户不需要从线程池中获取实体,只用将想要并发执行的任务提交给已经创建好的线程池对象即可,在线程池内部进行任务的竞争和执行
线程池的组成部分
- 停止标志(用于停止所有工作线程)
- 互斥锁(用于保护工作队列)
- 工作队列(存放客户要执行的任务对象)
- 工作线程(线程池中的线程)
- 辅助类(可以不要,不过就需要在析构函数手动调用 join())
辅助类实现
引用 std::vector<std::thread>
,保证当辅助类析构时所引用对象的线程都完成
// join_threads实例用于保证在其生命周期结束之前引用的线程容器中线程全部结束
class join_threads {
private:
std::vector<std::thread>& threads;
public:
explicit join_threads(std::vector<std::thread>& threads_)
: threads(threads_) {}
~join_threads() {
for (auto& thread : threads) {
if (thread.joinable()) { // 如果线程在工作就等待
thread.join();
}
}
}
};
线程池类实现
注意成员变量声明的顺序,stop
标志和 work_queue
必须声明在 threads
向量前面,threads
向量必须声明在 joiner
前面。这个顺序保证所有成员会被正确的销毁掉。
#include <thread>
#include <atomic>
#include <mutex>
#include <queue>
#include <functional>
class threadpool {
private:
std::atomic_bool stop; // 停止标记
std::mutex queue_mutex; // 保护队列的互斥量
std::queue<std::function<void()>> work_queue; // 任务队列
std::vector<std::thread> threads; // 线程容器
join_threads joiner;
void work_thread() {
while (!stop) {
std::unique_lock<std::mutex> lk(queue_mutex);
if (!work_queue.empty()) {
auto task = work_queue.front();
work_queue.pop();
lk.unlock();
task(); // 注意: 释放锁之后再执行任务, 而不是执行完任务再释放锁
}
else {
std::this_thread::yield(); // 调度当前线程将其放入等待队列的队尾
}
}
}
public:
threadpool() : stop(false), joiner(threads) {
// 线程池中线程的数量为硬件线程数(推荐)
unsigned int thread_count = std::thread::hardware_concurrency();
try { // 创建线程可能会抛出异常
for (unsigned int i = 0; i < thread_count; ++i) {
threads.push_back(std::thread(&threadpool::work_thread, this));
}
}
catch (...) { // 让其它正在运行的线程停止
stop = true;
throw; // 抛出异常让上一层处理
}
}
~threadpool() {
stop = true;
}
// 向线程池提交任务
template<typename Function>
void submit(Function f) {
std::lock_guard<std::mutex> lk(queue_mutex);
work_queue.push(std::function<void()>(f));
}
};
测试程序
#include <iostream>
#include <chrono>
#include "threadpool.h"
void task() {
std::cout << std::this_thread::get_id() << '\n';
}
int main() {
threadpool pool;
for (int i = 0; i < 15; i++) {
pool.submit(task);
}
// 让主线程等待1s, 等待任务都加入队列且完成
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
等待提交给线程池的任务
上面的实现有一个很明显的缺点,就是当提交任务的循环结束后,任务还未全部执行完主线程就跑完从而时任务中断的情况。这会导致无法与预期相符合,因此我们应该在主线程中等待提交的任务结束,特别是我们需要任务计算的结果的时候。我们可以通过 std::future
来等待线程的结束,由于 std::packaged_task<>
只能是移动的,而不可复制,故我们不能将其直接放入队列中,需要用定制的函数包装来处理,这里用到比较直观的虚调用
来实现
class function_wrapper {
private:
struct impl_base { // 抽象基类
virtual void call() = 0;
~impl_base() {}
};
std::unique_ptr<impl_base> impl; // 指向基类的指针
template <typename F>
struct impl_type : impl_base {
F f;
impl_type (F&& f_) : f(std::move(f_)) {} // 接受一个可调用对象作为参数
void call() override { f(); } // 多态实现
};
public:
template <typename F>
function_wrapper(F&& f) : impl(new impl_type<F>(std::move(f))) {}
// 重载 () 运算符
void operator()() { impl->call(); }
function_wrapper() = default;
// 支持移动操作
function_wrapper(function_wrapper&& other) : impl(std::move(other.impl)) {}
function_wrapper& operator=(function_wrapper&& other) {
impl = std::move(other.impl);
return *this;
}
// 禁用拷贝操作
function_wrapper(const function_wrapper&) = delete;
function_wrapper& operator=(const function_wrapper&) = delete;
};
将任务队列中的元素类型更换为 function_wrapper
即可,同样我们需要对 submit
方法做相应更改,现在我们可以从用户任务中接受返回值啦!
template<typename FunctionType>
auto submit(FunctionType f) {
typedef typename std::result_of<FunctionType()>::type result_type;
std::packaged_task<result_type()> task(std::move(f));
std::future<result_type> res(task.get_future());
std::lock_guard<std::mutex> lk(queue_mutex);
work_queue.push(std::move(task)); // 传入一个函数对象即可
return res;
}
注意:直接用 auto
作为返回值是c++14
之后的特性了,如果是 c++11
就不能偷懒了,需要将 auto
改为 std::future<typename std::result_of<FunctionType()>::type>
测试程序
#include <iostream>
#include "threadpool.h"
std::thread::id task() {
return std::this_thread::get_id();
}
int main() {
threadpool pool;
std::vector<std::future<std::thread::id>> ans;
for (int i = 0; i < 15; i++) {
ans.push_back(pool.submit(task));
}
for (int i = 0; i < 15; i++) {
std::cout << ans[i].get() << std::endl;
}
return 0;
}
参考书籍
《 Linux 高性能服务器编程 》
《 C++ 并发编程实战 》