本文作为C++线程池相关内容,读者需要有一定的C++知识储备及对并发编程的内容有所掌握。
通过学习,不仅可以掌握线程池的部分,同时也可对C++基础知识进行复习。
不仅仅是对线程池的讲解,同时会对线程池的衍生使用方法进行讲解实战。在项目出现的一些函数或者方法会做介绍,方便读者学习。
接下来进入正题:
一、线程池
1.1 什么是线程池?
线程池是 C++ 并发编程中管理线程资源的核心技术,它预先创建一组线程(称为 “工作线程”),这些线程不会在执行完单个任务后销毁,而是回到线程池等待下一个任务,从而避免反复创建 / 销毁线程的高昂开销(线程创建需分配栈空间、内核资源,销毁需回收资源,频繁操作会显著消耗系统性能)。
1.2为什么需要线程池?
在 C++ 开发中,直接使用std::thread创建线程存在以下问题:
- 资源浪费:频繁创建线程会导致内核态与用户态切换频繁,占用大量内存(每个线程默认栈大小通常为 1-8MB)。
- 并发失控:无限制创建线程可能导致 “线程爆炸”,引发 CPU 上下文切换过载、系统响应变慢甚至崩溃。
- 编程复杂:手动管理线程生命周期(创建、同步、销毁)会增加代码复杂度,易出现死锁、数据竞争等问题。
线程池通过 “预创建线程 + 任务队列” 的模式,完美解决上述问题,是高并发场景(如服务器、批量任务处理)的必备工具。
1.3线程池的核心组件与工作原理
|
模块名称 |
主要职责 |
|
线程池(ThreadPool) |
管理整个系统,创建线程、维护任务队列、调度执行、关闭资源。 |
|
任务队列(TaskQueue) |
用于存放待执行的任务,通常采用安全容器(如 |
|
工作线程(Worker) |
实际执行任务的线程,从任务队列中取任务并执行。 |
|
同步机制(锁+条件变量) |
保证线程之间访问共享队列时不出现竞争;条件变量用于任务到来时唤醒线程。 |
工作流程:
①提交任务
主线程调用 submit() 或类似接口,将任务(一般是函数或可调用对象)放入任务队列中。
同时通过 condition_variable.notify_one() 唤醒一个等待中的工作线程。
②工作线程等待与唤醒
每个工作线程在启动后会进入一个循环:
- 使用
condition_variable.wait()阻塞,等待任务队列中出现新任务; - 一旦有任务到来,条件变量被唤醒,线程获得锁,从队列中取出任务。
③任务执行阶段
线程取到任务后释放锁(避免阻塞其他线程取任务),执行该任务。
- 若任务抛出异常,一般需要在线程内捕获,以防止线程异常退出。
- 任务执行完毕后,线程继续回到等待状态。
④线程池关闭阶段
当主程序调用 shutdown() 或销毁线程池对象时:
- 设置停止标志
stop_flag = true; - 唤醒所有等待的线程;
- 每个线程检测到停止标志后退出循环;
- 主线程
join()所有子线程,确保安全退出。
1.4代码展示如下所示
代码前瞻:
ThreadPool类的数据结构:
使用queue作为任务队列:线程池的核心是按任务提交的顺序执行(先进先出,FIFO),因为
大多数场景下,任务的提交顺序与执行优先级无关,先提交的任务应优先被工作线程处理。
这种调度方式最简单、直观,能避免复杂的优先级管理,减少线程池的实现复杂度。而queue刚好满足这种要求,且它的接口简单,很适合线程池的核心操作。
vector作为工作线程,里面的数据类型为是std::thread。
stop_flag:标记线程池运行状态。也是实现析构的核心数据结构
从上至下依次说明:
首先是线程池Thread Pool的构造函数,在这里实现线程池。利用emplace_back在vector尾部构造线程,它的作用是减少内存开销(直接构造而不用复制数据),每调用一次就会创建并启动一个线程,并且在这个for循环中,线程执行不会影响线程创建。主线程创建线程 1 后,不等线程 1 进入阻塞,就会继续执行循环创建线程 2、3…… 直到所有线程创建完成。因为要将任务队列中的任务取出来,所以我们需要上锁,防止别的线程过来横插一脚对数据进行修改。接下来就是线程等待被唤醒,这里的逻辑是如果线程池不在运行或者线程池不为空,则线程被唤醒,包括下面的那行代码,是while(true)能够实现退出的关键:如果线程池运行标志stop_flag为真同时任务队列为空,则退出循环(直接退出当前的 while(true) 循环,并且会退出整个线程的执行函数(lambda 表达式)。)总结:该函数就是创建线程池并提取任务队列中的成员去执行它。
submit函数这里使用了万能引用和尾置返回类型。它作为类的外部接口,将需执行的任务打包后传入tasks任务队列中,所有线程均有可能获得执行这个任务的机会,所以在他们之前这个任务是共享的。这里的res(std::future对象)的作用就是在异步执行的任务和主线程之间建立可靠的通信渠道,确保结果不丢失、执行状态可感知、异常可处理。没有它,线程池只能 “盲目执行” 任务,无法满足实际开发中对任务结果和执行状态的需求。将任务添加到任务队列后,唤醒一个线程来执行它。
看懂了线程池的构造函数是如何实现退出后,对析构函数也能轻易的掌握。修改stop_flag标记实现退出所有的线程任务。最后一定要join掉所有线程。

输出结果如下所示:
可以看到两个执行结果不一样:一个是乱序的一个顺序的,其实如果多次执行结果大概率也是不一致的。为什么会导致输出结果不同呢?多个线程同时操作一个输出流,导致并发写冲突:比如线程A要输出Task 1 :done,线程B要输出Task 2 :done,由于上述原因,输出结果可能是这样的:Task 1: Task 2: done done。解决的方法很简单,就是在输出段加一个锁即可实现顺序输出。


1.5代码的补充
上述代码中有一些常见的问题 ,以下作为补充介绍
①为什么要捕获this:
如果不捕获this,lambda 里无法直接用queue_mutex或tasks—— 因为这些是类的成员,不是 lambda 的局部变量。
②为什么要无限循环?----这个是要点
因为线程池的核心是 “复用线程”:一个线程执行完一个任务后,不能销毁,要继续等下一个任务,所以用循环让线程 “一直活着” 并待机。while循环是保证每个线程都保持在线状态,不会被主动销毁。
③为什么要使用cv.wait()?
这是最关键的部分,避免线程空循环浪费CPU
它可以阻止线程在while(true)中疯狂循环,不满足或条件则解除当前锁,同时休眠线程,等待被唤醒;否则的话 每次循环都加锁→判断任务队列是否为空→解锁→再循环…… 即使队列空,也会反复加解锁,占用大量 CPU(空轮询) 。
具体流程:
具体流程:
- 先检查 “谓词”(
this->stop || !this->tasks.empty()):
-
- 若满足条件(比如任务队列非空):不休眠,直接往下执行(取任务)。
- 若不满足(队列空且没停止):释放锁→让线程休眠→直到被
cv.notify_one()/cv.notify_all()唤醒。
2.唤醒后:重新加锁→再次检查谓词→满足则继续执行,不满足则再次休眠。
在第一循环时,第一个线程wait阻塞,此时for循环开始执行第二次循环,开始构造第二个线程
1145

被折叠的 条评论
为什么被折叠?



