一、fixedthreadpool.hpp
#ifndef FIXEDTHREADPOOL_H
#define FIXEDTHREADPOOL_H
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <stdexcept>
class fixedthreadpool {
public:
fixedthreadpool(int thread_num);
~fixedthreadpool();
// 支持任意参数和返回值,返还给小票
template<typename f, typename... args>
auto enqueue(f&& func, args&&... args_) -> std::future<decltype(func(args_...))> {
using rettype = decltype(func(args_...));
// 利用智能指针把任务打包成shared_ptr<packaged_task>,方便后续调用
// 这里踩过坑:一开始用unique_ptr,后面发现无法传递,改成shared_ptr
auto task = std::make_shared<std::packaged_task<rettype()>>(
std::bind(std::forward<f>(func), std::forward<args>(args_)...)
);
//获得结果
std::future<rettype> res = task->get_future();
{
std::lock_guard<std::mutex> lock(mtx);
if (!is_running) {
throw std::runtime_error("thread pool has stopped, can't enqueue task");
}
task_queue.push([task]() { (*task)(); });
}
cv.notify_one();
return res;
}
void stop();
private:
// 工作线程的主循环函数,每个线程都会执行这个函数
void worker();
std::vector<std::thread> threads; // 存储所有工作线程
std::queue<std::function<void()>> task_queue; // 任务队列
matable std::mutex mtx; // 保护任务队列的互斥锁
std::condition_variable cv; // 条件变量,用于线程等待/唤醒
bool is_running; // 线程池运行状态标记
};
#endif
二、fixedthreadpool.cpp
几个关键踩坑点(重点记):
-
worker函数中,必须用std::unique_lock而不是std::lock_guard,因为条件变量cv.wait()需要解锁,lock_guard不能手动解锁,unique_lock可以;
-
cv.wait()的第二个参数是"唤醒条件",避免虚假唤醒------比如线程被唤醒后,发现队列还是空的,就继续等待;
-
停止线程池时,要先设置is_running为false,再唤醒所有线程,避免有线程还在等待,无法退出;
-
线程执行任务时,要加try-catch,避免单个任务抛出异常,导致整个线程崩溃;
-
析构函数中要判断is_running,如果线程池还在运行,就调用stop(),防止线程泄漏。
cpp
#include "fixedthreadpool.hpp"
#include <iostream>
// 初始化线程池
fixedthreadpool::fixedthreadpool(int thread_num) {
if (thread_num <= 0) {
thread_num = 1;
}
is_running = true;
// 启动指定数量的工作线程
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(&fixedthreadpool::worker, this);
}
// 调试用的cout,注释掉
// std::cout << "fixed thread pool start, thread num: " << thread_num << std::endl;
}
fixedthreadpool::~fixedthreadpool() {
if (is_running) {
stop();
}
}
void fixedthreadpool::worker() {
// 循环取任务
while (true) {
// 包装一个任务
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
// 等待条件:队列不为空 或者 线程池停止
// 这里踩过坑:一开始没写|| !is_running,导致线程池停止后,线程还在等待,无法退出
cv.wait(lock, [this]() {
return !task_queue.empty() || !is_running;
});
// 如果线程池停止,并且任务队列为空,就退出循环,结束
if (!is_running && task_queue.empty()) {
return;
}
// 从队列中取出任务,注意用std::move,避免拷贝,提高效率
task = std::move(task_queue.front());
task_queue.pop();
}
// 执行任务,加try-catch,防止任务异常导致线程崩溃
try {
task();
} catch (const std::exception& e) {
// 打印异常信息,方便调试
std::cerr << "task execute error: " << e.what() << std::endl;
}
}
}
void fixedthreadpool::stop() {
{
// 加锁,避免多线程同时修改
std::lock_guard<std::mutex> lock(mtx);
is_running = false;
}
cv.notify_all();
// 等待所有线程执行完毕,回收线程资源
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
// std::cout << "fixed thread pool stopped" << std::endl;
}
// 补充:这里可以加一个简单的测试函数,方便自己调试
// 我自己调试时写的,注释掉,需要测试时打开
/*
void test_task(int num) {
std::cout << "task " << num << " execute, thread id: " << std::this_thread::get_id() << std::endl;
// 模拟任务耗时
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
int main() {
try {
fixedthreadpool pool(4);
// 提交10个任务,测试线程池是否正常工作
for (int i = 0; i < 10; ++i) {
pool.enqueue(test_task, i);
}
// 等待任务执行完毕
std::this_thread::sleep_for(std::chrono::seconds(1));
pool.stop();
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << std::endl;
return 1;
}
return 0;
}
*/
三、重点知识点讲解
1. 线程创建写法解释
cpp
thread_group.emplace_back(&fixedthreadpool::worker,this);
很多新手疑惑,为什么不能直接只写 worker,必须加 &fixedthreadpool::
- worker 不是全局普通函数,它是类内部私有成员函数,单独写 worker 编译器找不到这个函数
fixedthreadpool::是作用域限定符,明确告诉编译器这个 worker 属于 fixedthreadpool 这个类- 前面的
&作用是取出成员函数的内存地址,线程创建需要传入函数地址 - 末尾的 this 代表当前线程池对象,成员函数必须绑定对象才能调用内部成员变量
简单总结:全局函数直接写名字就行,类内成员函数创建线程必须带上 类名::加取地址符号。
2. 锁的使用区别
- lock_guard:简单自动加锁解锁,无法手动解锁,只适合短时间锁定代码
- unique_lock:灵活锁,支持手动解锁、等待休眠,搭配条件变量 cv 必须用这个
3. 线程休眠唤醒逻辑
条件变量 wait 函数会自动释放互斥锁,线程进入休眠状态,等到有新任务提交调用 notify_one,就会唤醒一条空闲线程去执行任务,极大减少 cpu 空转消耗。
4. 停止逻辑
修改运行标记为关闭状态,唤醒所有休眠线程,让所有线程走完循环正常退出,最后调用 join 回收线程资源,不会出现内存泄漏。
四、简单测试代码
cpp
#include "fixedthreadpool.hpp"
#include <chrono>
#include <iostream>
void testwork(int num)
{
std::cout<<"执行任务编号:"<<num<<" 线程id:"<<std::this_thread::get_id()<<std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
int main()
{
fixedthreadpool pool(3);
for(int i = 1;i <= 8;i++)
{
pool.enqueue(testwork,i);
}
std::this_thread::sleep_for(std::chrono::seconds(2));
pool.stop();
return 0;
}
五、固定线程池优缺点总结
优点
- 逻辑简单易懂,上手最快,适合新手入门学习
- 线程数量固定,系统资源占用平稳,不会突然大量创建线程拖垮程序
- 代码量少,维护简单,稳定性高
缺点
- 线程数量固定无法动态调整,任务少时浪费线程资源
- 高并发大量任务堆积时,队列容易积压,处理速度变慢
- 不适合突发大量短时 io 任务场景
六、学习总结
写完固定线程池之后,彻底理清了线程池最核心的运行逻辑:线程复用、任务队列排队、互斥锁保证安全、条件变量实现休眠唤醒。
这也是后续学习缓存线程池、优先级线程池、工作窃取线程池的基础,弄懂这一套基础逻辑,后续其他线程池只需要改动任务队列规则、线程创建销毁规则即可。