项目介绍
线程池是库的形式提供给用户,是必须放到代码中,不能单独运行,亦称为基础组件
第一版线程池任务对象使用继承技术,提供一个抽象基类Task
,里面有一个纯虚函数run()
,使用时继承该类,并重写该纯虚函数,在这个重写的纯虚函数中完成要执行的具体任务;
第二版线程池使用C++11 的std::packaged_task<T>
来包装任务执行函数,用std::future<T>
来接受任意类型的结果
完整源代码
https://github.com/StrikeCode/cpp11_threadpool.git
技术点
熟练基于C++ 11标准的面向对象编程
组合和继承、继承多态、STL容器、智能指针、函数对象、绑定器、可变参模板编程等。
熟悉C++11多线程编程
thread
、mutex
、atomic
、condition_variable
、unique_lock
等。
C++17和C++20标准的内容
C++17的any类型和C++20的信号量semaphore
,项目上都我们自己用代码实现。
熟悉多线程理论
多线程基本知识、线程互斥、线程同步、原子操作、CAS等。
线程池优点
服务进程启动之初,就实现创建好线程池,当业务流量到来时,需要线程就可以直接从线程池获取线程执行业务处理,不需要来一个任务创建一个线程,执行完任务再销毁线程,减少了线程的创建与销毁的开销
fixed线程池模式
线程池里面的线程个数是固定不变的,一般是ThreadPool
创建时根据当前机器的CPU核心数量进行指定。
cached模式线程池
线程池里面的线程个数是可动态增长的,根据任务的数量动态的增加线程的数量,但是会设置一个线程数量的阈值(线程过多的坏处上面已经讲过了),任务处理完成,如果动态增长的线程空闲了60s还没有处理其它任务,那么关闭线程,保持池中最初数量的线程即可
多线程优势
多线程程序就一定好吗?不一定,需要具体分析场景
IO密集型
无论是CPU单核或者CPU多核还是多CPU,都是适合多线程(因为IO多就会造成CPU空闲)
CPU密集型
CPU单核情况下,不适合多线程,因为多线程存在上下文切换的开销,不如单线程直接执行完成,因为CPU密集型场景也不怎么到等待状态
线程池的开销
为了完成任务,创建很多的线程可以吗?
- 线程的创建和销毁都是非常"重"的操作, 需要创建相应的数据结构如PCB task_struct,需要描述地址空间相应的数据结构 vm_struct、vm_area_struct,需要创建相应的页目录和页表项;
- 线程栈(线程执行函数会使用到)本身占用大量内存,32位机一般一个进程最多只能创建380个线程左右,因为:
- 假设一个线程需要8M内存空间,一个进程的虚拟内存用户空间为3G,3G/8M = 384;
- 线程的上下文切换要占用大量时间;
- 大量线程同时唤醒会使系统经常出现锯齿状负载或者瞬间负载量很大导致宕机,比如同一时间很多IO操作的已经完成
线程同步
竞态条件
指设备或系统临界区代码段出现不恰当的执行时序,而得到不正确的结果
如果多线程环境下不存在竞态条件,则称为可重入(多线程)的代码段,反之称为不可重入的代码段
线程互斥
-
互斥锁
-
atomic
原子类型,一般利用CAS操作(无锁机制)实现,常见应用:无锁队列、链表、数组(使用活锁)
线程通信
条件变量 condition_variable
使用方法:mutex
+condition_variable
wait
和signal
的交互状态关系:
问题:用notify_one还是notify_all? notify_one可能会死锁
- 信号量 semaphore
可以用条件变量封装一个信号量
信号量可看作资源计数没有限制的互斥量:
mutex.lock() //锁的资源计数1 -》0
critical section
mutex.unlock()//锁的资源计数0 -》1
控制的精度不如条件变量
用于保证操作时序,而没有对资源的互斥访问
使用wait (-1)和 post(+1) 方法