作为五大池之一(内存池、连接池、线程池、进程池、协程池),线程池的应用非常广泛,不管是客户端程序,还是后台服务程序,都是提高业务处理能力的必备模块。有很多开源的线程池实现,虽然各自接口使用上稍有区别,但是其核心实现原理都是基本相同的。
- CPU单核
- CPU多核、多CPU
单核上,多个线程占用不同的CPU时间片,物理上还是串行执行的,但是由于每个线程占用的CPU时间非常短(比如10ms),看起来就像是多个线程都在共同执行一样,这样的场景称作为并发(concurrent)。
在多核或者多CPU上,多个线程是在真正的同时执行,这样的场景称作并行(parallel)。
多线程程序一定就好吗?不一定,要看具体的应用场景:
程序里面的指令涉及一些IO操作,比如设备、文件、网络操作(等待客户端的连接IO操作是可以把程序阻塞住的,此时再分配给这样的程序时间片,CPU相当于空闲下来了)。
无论是CPU单核、CPU多核、多CPU,都是比较适合多线程程序的。
程序里面的指令都是做计算用的,比如从1加到100。
- CPU单核
多线程存在上文切换,是额外的花销,线程越多上下文切换所花费的额外时间也越多,倒不如一个线程一直进行计算。
- CPU多核、多CPU
多个线程可以并行执行,对CPU利用率好。
为了完成任务,创建很多的线程可以吗?线程真的是越多越好吗?
- 线程的创建和销毁都是非常“重”的操作
- 线程栈本身占用很大内存
- 线程的上下文切换要占用大量时间
- 大量线程同时唤醒会使系统经常出现锯齿状负载或者瞬间负载量很大导致宕机
操作系统上创建线程和销毁线程都是很“重”的操作,耗时耗性能都比较多,那么在服务执行的过程中,如果业务量比较大,实时的去创建线程、执行业务、业务完成后销毁线程,那么会导致系统的实时性能降低,业务的处理能力也会降低。
线程池的优势就是(每个池都有自己的优势),在服务进程启动之初,就事先创建好线程池里面的线程,当业务流量到来时需要分配线程,直接从线程池中获取一个空闲线程执行task任务即可,task执行完成后,也不用释放线程,而是把线程归还到线程池中继续给后续的task提供服务。
线程池里面的线程个数是固定不变的,一般是ThreadPool创建时根据当前机器的CPU核心数量进行指定。
线程池里面的线程个数是可动态增长的,根据任务的数量动态的增加线程的数量,但是会设置一个线程数量的阈值(线程过多的坏处上面已经讲过了),任务处理完成,如果动态增长的线程空闲了60s还没有处理其他任务,那么关闭线程,保持池中最初数量的线程即可。
-
互斥锁mutex
-
atomic原子类型
- 条件变量condition_variable
- 信号量semaphore
这是一个C++11/14手写线程池实现,包含固定(fixed)与可缓存(cached)两种模式。代码演示了任务提交、任务执行、结果获取、以及基于互斥量与条件变量的线程间通信机制。
- 线程管理(Thread / ThreadPool):
Thread封装线程创建与detach行为,ThreadPool负责维护线程容器、调度与回收。 - 任务抽象(Task / Any / Result):用户通过继承
Task并实现run()提交任务;Any提供一个简单的类型擦除容器用于保存任意返回值;Result用于在主线程等待并获取任务执行结果。 - 同步机制:使用
std::mutex+std::condition_variable管理任务队列的“满/空”状态,使用自实现的Semaphore在Result::get()中等待任务完成。
关键设计点:
- 支持两种运行模式:
PoolMode::MODE_FIXED(线程数固定)和PoolMode::MODE_CACHED(线程在任务高峰期扩容,空闲超时后回收)。 - 使用
std::queue<std::shared_ptr<Task>>存放待执行任务,通过submitTask将任务入队并通知工作线程。 submitTask在队列满时会使用condition_variable::wait_for限时等待(代码中为1秒),等待超时则返回一个不可用的Result。
Any:简易类型擦除容器,支持cast_<T>()获取值。Semaphore:基于条件变量实现的计数信号量,用于Result的同步。Task/Result:任务接口与结果容器,Result::get()会阻塞直到任务通过setVal()写入返回值。Thread:封装线程启动(detach())与id生成。ThreadPool:核心调度器,负责接收任务、唤醒工作线程、动态扩容/回收(cached模式)。
- 使用Visual Studio打开工程:打开ThreadPoolProject/ThreadPoolProject.sln,选择Debug/x64后编译并运行。
- 在类Unix下可以使用g++编译(需稍作移植,当前代码以VS为主):
g++ -std=c++11 ThreadPoolProject/threadpool.cpp ThreadPoolProject/main.cpp -O2 -pthread -o threadpool_demo
./threadpool_demo程序运行时控制台会打印线程获取与执行日志,示例片段:
tid:140711... begin!
tid:140711... end!
>>> create new thread...
task queue is full, submit task fail.
1234567890 # 任务返回的计算结果(示例)
main over!
- 高并发网络服务器
- master-slave线程模型
- 耗时任务处理
项目名称:基于可变参模板实现的线程池
github地址:https://github.com/Belief-hub/ThreadPool
平台工具:vs2022开发,ubuntu22.04编译so库,gdb调试分析定位死锁问题
项目描述:
1、基于可变参模板编程和引用折叠原理,实现线程池submitTask接口,支持任意任务函数和任意参数的传递
2、使用future类型定制submitTask提交任务的返回值
3、使用map和queue容器管理线程对象和任务
4、基于条件变量condition_variable和互斥锁mutex实现任务提交线程和任务执行线程间的通信机制
5、支持fixed和cached模式的线程池定制
项目问题:
1、在ThreadPool的资源回收,等待线程池所有线程退出时,发生死锁问题,导致进程无法退出
2、在windows平台下运行良好的线程池,在linux平台下运行发生死锁问题,平台运行结果有差异化
分析定位问题:
主要通过gdb attach到正在运行的进程,通过info threads,thread tid,bt等命令查看各个线程的调用堆栈信息,结合项目代码,定位到发生死锁的代码片段,分析死锁问题发生的原因,以及最终的解决方案。
