线程池:提升性能与控制并发的利器 什么是线程池有什么用处一种预先创建一组线程的机制这些线程在程序启动时就已经创建好了等待执行线程当有新任务需要执行时线程池会从线程集合中分配一个空闲线程来执行该任务而不是每一个任务都分配新线程为什么要使用线程池①提高性能创建和销毁线程是开销较大的操作通过重用现有线程可以减少开销②控制并发量线程池允许限制并发的数量防止因创建线程过多而出现资源耗尽的情况cpu过载、内存不足③简化线程管理线程池可以避免手动管理线程的声明周期。线程池通常提供了任务排队和任务调度的功能使得多线程编程更加简化使用场景1、服务器应用套接字通信web服务器2、高性能计算线程池可以帮助分配管理计算任务3、异步任务处理线程池原理图实现前的前置知识①C11语法1. lambda表达式语法[捕获列表](参数列表) - 返回类型 {函数体};优点① 可以直接在需要的地方定义不用单独写函数。② 可以访问周围作用域的变量。③ 可以作为参数传给函数比如标准库算法。捕获方式小技巧捕获方式意思[a]按值捕获只读内部修改不会影响外部[a]按引用捕获可以修改外部变量[]捕获所有外部变量按值[]捕获所有外部变量按引用2. 智能指针unique_ptr \ shared_ptr \ weak_ptr遵循RAII思想 对象创建时获取资源对象销毁时释放资源 普通指针你自己负责开门关门。智能指针它帮你开门、关门而且不会忘。类型作用特点std::unique_ptrT独占所有权不能拷贝只能移动析构时自动delete指针std::shared_ptrT共享所有权引用计数reference count最后一个shared_ptr被销毁时释放内存std::weak_ptrT弱引用不增加引用计数用于打破循环引用通常与shared_ptr搭配使用3. 移动语义语法std::move()为什么需要移动语义先看下面的例子std::string s1 Hello, World!; std::string s2 s1; // 复制内容s1的内容被完整复制到s2对大对象比如超大的vector、string很浪费内存和性能移动语义就是为了解决这个问题把资源“搬过来”而不是复制一份。移动语义的核心概念右值Rvalue临时对象或者不再需要的对象左值Lvalue有名字的、可以再次使用的对象#include iostream #include vector using namespace std; int main() { vectorint v1 {1,2,3,4,5}; // 移动 v1 的内容到 v2 vectorint v2 std::move(v1); cout v1 大小: v1.size() endl; // 0 cout v2 大小: v2.size() endl; // 5 return 0; }std::move(v1)把v1转为右值引用v2的移动构造函数直接接管了v1的内存v1变成空的但不报错②多线程编程1. 互斥锁mutex申请语法std::mutex为什么需要互斥锁在多线程编程中经常会出现共享资源被多个线程同时访问的情况 比如多个线程同时修改一个全局变量或写入文件这种情况叫做竞态而互斥锁mutex就是用来保护共享资源保证一次只有一个线程可以访问。手动lock/unlock容易忘记C 提供了RAII 风格的锁std::lock_guardstd::mutexlock(mtx);也可以配合智能指针使用std::unique_ptrstd::mutex lock(mtx); std::shared_ptrstd::mutex lock(mtx);2. 条件变量condition_variable )std::condition_variablecv;cv.notify_one();cv.notify_all();cv.wait();为什么需要条件变量假设你有两个线程生产者线程负责生成数据比如任务、消息消费者线程负责使用数据如果没有条件变量消费者就只能不停地检查数据有没有准备好叫做忙等CPU白白浪费。有了条件变量消费者“睡觉”等数据来了有人叫醒它。生产者生成数据后“叫醒”消费者。这就节省了资源而且线程更高效。③STL1、push_back() V.S. emplace_back()push_back作用把一个已经存在的对象拷贝或移动到容器的末尾。emplace_back作用直接在容器末尾原地构造对象避免先创建再拷贝/移动。2、functionalstd::function作用将一个函数封装成对象① threadPool.h线程池需要一个存储线程的容器一个任务队列线程池的状态表示以及内置的互斥锁和条件变量。需要提供内置的启动线程池的函数和添加任务的函数。#ifndef THREADPOOL_H #define THREADPOOL_H #includeiostream #includevector //用于存放多个线程 #includethread //线程相关 #includequeue //存储多个任务的队列 #includecondition_variable //条件变量 #includemutex //互斥锁 #includefunctional #includemyhead.h class threadPool { private: //线程池相关私有成员 std::vectorstd::thread workers;//存储工作的线程容器 std::queuestd::functionvoid() tasks;//存储任务的队列 std::mutex task_mutex;//互斥锁 std::condition_variable task_cv;//用于通知线程有新任务的条件变量 bool stop; //线程池的状态标志 /// brief 启动线程池 /// param 线程数量 void startThreadPool(size_t numThreads); public: threadPool(int threadPoolSize); ~threadPool(); /// brief 添加任务到线程池 /// param functionvoid()对象 void addTask(std::functionvoid()); }; #endif②threadPool.cpp析构函数threadPool::~threadPool() { { unique_lockmutex lock(task_mutex);//获取锁资源用于保护stop变量 stop true; } //将所有等待线程唤醒 task_cv.notify_all(); //将所有的工作线程回收 for(auto x:workers){ x.join(); } }执行逻辑① 加锁修改线程池状态花括号{}创建了一个局部作用域。unique_lockmutex lock(task_mutex);为task_mutex上锁保护共享变量stop的访问保证线程安全设置stop true;标记线程池停止工作所有工作线程看到stop true后会逐步退出花括号结束 →lock被析构 → 自动解锁使用RAIIunique_lock自动加锁/解锁避免忘记解锁导致死锁。② 唤醒所有等待线程notify_all()会唤醒所有阻塞在wait上的线程每个线程醒来后会重新检查看到stop true就会停止工作③ 回收工作线程x.join()等待线程x执行完毕阻塞当前线程析构函数所在线程直到工作线程退出开启线程池函数void threadPool::startThreadPool(size_t numThreads){ //循环创建指定数量的线程 for(int i 0;inumThreads;i){ workers.emplace_back([this]{ //使用lambda表达式定义线程体 while(1){ functionvoid() task; //创建一个任务 { unique_lockmutex lock(task_mutex);//加锁保护任务队列 task_cv.wait(lock,[this]{ //等待条件变量通知 return stop||!tasks.empty();//当线程池停止工作或者任务队列不为空时执行 }); if(stop tasks.empty()){ return; //当线程池停止并且任务队列为空退出线程 } task move(tasks.front()); //从任务队列中取出一个任务 tasks.pop();//将任务从任务队列中移除 } task();//执行任务 } }); } }执行逻辑① 外层循环创建线程循环numThreads次每次创建一个线程。workers.emplace_back(...)在workers容器里添加一个std::thread对象。线程体是lambda 表达式捕获this即线程池对象本身。所以到这里你启动了numThreads个线程每个线程开始执行 lambda 里的循环逻辑。② 线程体循环无限循环等待任务③ 临界区锁 条件变量等待加锁task_mutex 确保多个线程访问tasks队列时不会冲突。条件变量等待task_cv.wait(...)线程会 阻塞等待直到满足 lambda 条件如果线程池stop true→ 可以退出循环如果tasks队列非空→ 有任务可以执行wait内部会自动释放锁让其他线程能添加任务。被唤醒时会重新 获取锁再检查条件。④ 取出任务并移除执行任务从队列中取第一个任务move可以避免拷贝提高效率然后从队列中删除pop防止重复执行注意这一步必须在锁内完成确保线程安全。