CPP多线程编程范式思考
本篇文章用来记录阅读Linux多线程服务端编程的学习笔记,采用知识点加范式的形式来记录。Item记录范式,知识点直接记录。
Item1:不要在构造函数中泄露this指针
Item2:构建线程安全的析构函数是困难的且重要的
作为数据成员的mutex不能保护析构
share_ptr/weak_ptr 多线程下资源资源管理神器
在说明share_ptr的底层时,最重要的一点就是proxy思想,没错,解决多线程资源释放竞态问题的关键就是代理释放,将释放动作代理给share_ptr,而不是对象自己的析构函数,这样就能保证释放动作在没有对象引用时执行,这也是对象引用计数设计的由来。
有人一阵见血的指出share_ptr的作用:保证内存中所有的对象都是有用的,也就是有被持有引用。
如何设计share_ptr / weak_ptr?由owner持有指向child的share_ptr,child持有指向owner的weak_ptr,这样就能保证owner在释放时,child也能释放,而child释放时,owner不会释放。
Item3:RAII教条:每一个明确的资源配置动作都应该在单一语句中执行,并在该语句中立即将配置的获得的资源交给handle对象,程序中不再出现delete
对象池
对象池内持有储存对象共享指针的容器,存在问题,持有对象的共享指针会导致对象永远无法释放,所以需要储存对象的弱指针的容器。弱指针还有个问题,就是弱指针对象一直存在,导致容器的size只增不减,这时候就需要使用share_ptr的定制析构功能,在weak_ptr对应的share_ptr所指对象析构时,对容器进行删除操作。
share_ptr的定制析构功能,也叫析构回调函数。
任何回调函数都存在一个问题:就是回调对象可能已经不存在了,当然这个问题是可以解决的,就是弱回调技术。弱回调技术需要使用share_from_this。某个类继承了enable_shared_from_this,就能使用share_from_this,使得this指针也能变成share_ptr,延长生命周期。但是如果每个回调中都持有回调对象的share_ptr,那么就可能导致回调对象永远无法释放,所以需要使用weak_ptr。也就是弱回调技术。
也就是传入share_from_this降级后的weak_ptr,如果回调对象存在,就操作回调对象;如果不存在,就忽略。这个技术在事件通知中非常有用,事件通知本质上就是回调函数调用。
Item4:尽量最低限度的共享对象,减少需要同步的场合;一个对象能不暴露给别的线程就不要暴露;如果要暴露,优先考虑immutable对象;实在不行才暴露可修改的对象,并用同步措施来充分保护它
Item5:使用高级并发编程构件,不要自己手搓轮子
Item6:最后不得已必须使用底层同步原语时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
使用mutex原语同样具有几个原则:
- 使用RAII原则来封装mutex的创建、销毁、加锁、解锁
- 不用重入的mutex
- 不手动调用lock和unlock,交给lock_guard
- 再每次构造guard对象时,思考调用栈上已经加的锁,防止因加锁顺序不同导致死锁。
- 不使用跨进程的mutex,进程间通信只通过socket
- 加锁解锁在同一个线程
Item7: 不要自己去编写lock-free代码,也不要使用内核级的同步原语
线程编程模型
单线程服务器的常用编程模型
Reactor和Proactor模型
多线程服务器的常用编程模型
- 每个请求创建一个线程,使用阻塞式IO操作
- 使用线程池,同样使用阻塞式IO操作
- 使用非阻塞式IO及IO多路复用
- Leader/Follower模式
第三种模型是比较常用的多线程C++网络服务程序的设计模式,其核心思想是 One Thread Per Loop。
One Thread Per Loop
每个IO线程有一个event loop(Reactor),处理读写和定时事件。好处如下:
- 线程数目基本固定,可以在程序启动的时候设置,不会频繁的创建和销毁
- 可以很方便的在线程间调配负载
- IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发
线程分为三种线程:IO线程、第三方库调用线程、计算线程。对于IO密集的任务可以分配到IO线程中,这个线程负责处理异步IO;对于数据处理任务可以分摊到几个计算线程中,使用线程池来分配;对于第三方库可能会指定你需要在某个线程中调用,这个时候就需要在这个线程中调用。
线程池
对于没有IO而光有计算任务的线程,使用event loop有点浪费,可以用任务队列 / 消息队列来实现。
总结:cpp多线程网络服务可以采用one loop per thread + thread pool。event loop 用作IO多路复用;thread pool用来做计算
Item8:进程间通信只用TCP
原因是可以跨机器,可扩展性强。
Item9:线程不是开越多越好
计算线程不能阻碍到IO线程,IO线程保证响应速度,如果计算线程开太多,导致挤占IO线程的cpu资源,导致对客户端的响应速度变慢,这就不是我们所想要看到的。还有性能上限可能不是CPU决定的,而是网络带宽或者硬盘IO决定的,这个时候开多线程就没有必要了。
对于多线程编程的创建问答的思考
多线程能提高并发量吗?
如果指的是并发连接数,答案是不能。开的线程数是有上限的;但如果是one loop per thread模式,一个loop理论上可以支持1万个并发长连接。
多线程能提高吞吐量吗?
对于计算密集型服务,答案是不能。因为计算是并发进行的,不会因为多线程而提高吞吐量。
多线程能降低响应时间吗?
答案是可以的。计算任务可以在多个线程中并发执行,降低响应时间。
什么是线程池的阻抗匹配原则?
如果池中线程执行任务时,密集计算所占的时间比重为P,而系统一共有C个CPU,为了让这C个CPU跑满而不过载,线程池大小的经验公式为T = C / P。这个是个经验公式。如果P小于0.2,那么这个公式就不适用了,T可以取个固定值,比如5*C。