Linux线程池
线程池设计线程池的概念⼀种线程使⽤模式。线程过多会带来调度开销进⽽影响缓存局部性和整体性能。⽽线程池维护着多个线程等待着监督管理者分配可并发执⾏的任务。线程池的优点这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利⽤还能防⽌过分调度。可⽤线程数量应该取决于可⽤的并发处理器、处理器内核、内存、⽹络sockets等的数量。线程池的应用场景线程池常见的应用场景如下1.需要大量的线程来完成任务且完成任务的时间比较短。2.对性能要求苛刻的应用比如要求服务器迅速响应客户请求。3.接受突发性的大量请求但不至于使服务器因此产生大量线程的应用。相关解释1.像Web服务器完成网页请求这样的任务使用线程池技术是非常合适的。因为单个任务小而任务数量巨大你可以想象一个热门网站的点击次数。2.对于长时间的任务比如Telnet连接请求线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。3.突发性大量客户请求在没有线程池的情况下将产生大量线程虽然理论上大部分操作系统线程数目最大值不是问题但短时间内产生大量线程可能使内存到达极限出现错误。线程池的核心价值是抵消 “线程创建 / 销毁” 的巨大开销。任务越短、越多这个价值越大任务越长、越少这个价值越小。线程池的实现下面我们实现一个简单的线程池线程池中提供了一个任务队列以及若干个线程多线程。创建固定数量线程池循环从任务队列中获取任务对象获取到任务对象后执⾏任务对象中的任务接⼝#pragmaonce#includeiostream#includequeue#includevector#includeunistd.h#includeMutex.hpp#includeCond.hpp#includeThread.hpp#includeLog.hppconststaticintdefaultthreadnum3;templateclassTclassThreadPool{private:// 判断队列是否为空boolQueueIsEmpty(){return_q.empty();}// 线程入口函数工作函数voidRoutine(conststd::stringname){while(true){T t;{// 锁只保护队列操作LockGuardlockguard(_lock);// 【核心】没有任务 线程池运行中 → 休眠等待// Stop() 唤醒后会重新检查这个条件while(_is_runningQueueIsEmpty()){_wait_thread_num;_cond.Wait(_lock);// 线程休眠在这里_wait_thread_num--;}// 【优雅退出】关闭 队列为空 → 线程退出if(!_is_runningQueueIsEmpty()){LOG(LogLevel::INFO)线程池退出 任务队列为空, name 退出;break;}// 取出任务t_q.front();_q.pop();}// 锁外执行任务高并发关键t();LOG(LogLevel::DEBUG)name handler task success;}}public:// 构造函数ThreadPool(intthreadnumdefaultthreadnum):_threadnum(threadnum),_wait_thread_num(0),_is_running(false){// 创建线程对象还没启动for(inti0;i_threadnum;i){std::string nameThread-std::to_string(i1);Threadt([this](conststd::stringname){this-Routine(name);},name);_threads.emplace_back(std::move(t));}LOG(LogLevel::INFO)线程池构造成功总线程数_threadnum;}// 启动所有线程voidStart(){if(_is_running)return;_is_runningtrue;for(autot:_threads){t.Start();}LOG(LogLevel::INFO)线程池启动成功;}// 等待所有线程退出voidWait(){for(autot:_threads){t.Join();}LOG(LogLevel::INFO)所有线程已安全退出线程池等待完成;}// 【核心】优雅关闭// 唤醒所有休眠线程 → 让它们检查退出条件voidStop(){if(!_is_running)return;_is_runningfalse;// 唤醒所有阻塞在 Wait 的线程if(_wait_thread_num0)_cond.NotifyAll();LOG(LogLevel::INFO)线程池已发送停止信号唤醒所有线程;}// 向线程池添加任务voidEnqueue(constTt){if(!_is_running)return;{LockGuardlockguard(_lock);_q.push(t);}// 有等待线程唤醒一个来处理if(_wait_thread_num0){_cond.NotifyOne();}}~ThreadPool(){}private:std::queueT_q;// 任务队列std::vectorThread_threads;// 线程数组int_threadnum;// 总线程数int_wait_thread_num;// 正在等待任务的线程数Mutex _lock;// 队列互斥锁Cond _cond;// 条件变量bool_is_running;// 线程池运行状态};为什么线程池中需要有互斥锁和条件变量线程池中的任务队列是会被多个执行流同时访问的临界资源因此我们需要引入互斥锁对任务队列进行保护。线程池当中的线程要从任务队列里拿任务前提条件是任务队列中必须要有任务因此线程池当中的线程在拿任务之前需要先判断任务队列当中是否有任务若此时任务队列为空那么该线程应该进行等待直到任务队列中有任务时再将其唤醒因此我们需要引入条件变量。当外部线程向任务队列中Push一个任务后此时可能有线程正处于等待状态因此在新增任务后需要唤醒在条件变量下等待的线程。注意•当某线程被唤醒时其可能是被异常或是伪唤醒或者是一些广播类的唤醒线程操作而导致所有线程被唤醒使得在被唤醒的若干线程中只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件所以在判断任务队列是否为空时应该使用while进行判断而不是if。•pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程而外部可能只Push了一个任务我们却把全部在等待的线程都唤醒了此时这些线程就都会去任务队列获取任务但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡这叫做惊群效应。因此在唤醒线程时最好使用pthread_cond_signal函数唤醒正在等待的一个线程即可。•当线程从任务队列中拿到任务后该任务就已经属于当前线程了与其他线程已经没有关系了因此应该在解锁之后再进行处理任务而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间所以我们不要将其放到临界区当中。•如果将处理任务的过程放到临界区当中那么当某一线程从任务队列拿到任务后其他线程还需要等待该线程将任务处理完后才有机会进入临界区。此时虽然是线程池但最终我们可能并没有让多线程并行的执行起来。•为什么创建线程必须 Lambda 捕获 this// 你写的void Routine(const string name)// 真实void Routine(ThreadPool* this, const string name)线程要求的函数类型只有 1 个 string 参数和成员函数 2 个参数不匹配。所以必须用 Lambda[this]捕获线程池对象Lambda 对外只暴露 string 参数匹配线程类型Lambda 内部调用this-Routine(name)补齐隐藏 this 参数成员函数永远自带 this必须 Lambda 包一层才能做线程回调。•为什么任务t()必须在锁外面执行锁只保护任务队列_q 的入队、出队操作不保护任务执行。锁内执行锁长期占用其他线程无法提交 / 取任务退化成单线程锁外执行取完任务立刻释放锁多线程真正并发任务内部自己的临界资源任务自己加锁保护和线程池锁无关互不干扰•Stop 优雅关闭完整逻辑关闭思想不暴力杀死线程让线程走正常唤醒逻辑自己退出关闭流程Stop 设置_is_running false调用NotifyAll()唤醒所有阻塞在 wait 处休眠的线程线程被唤醒位置_cond.Wait(_lock)这一行线程唤醒后重新判断已关闭 队列为空 → 线程退出已关闭但队列还有任务 → 继续把任务执行完再退出正在执行任务的线程跑完当前任务回到循环检查清空队列再退出唤醒不是为了让线程立刻退出是让线程来检查是否可以退出队列有任务一定先消费完不会丢任务、不会炸。