前言
最近在读陈硕的moduo网络库的书,记录总结一些东西。
读完本章,最大的收获是无论是CPU密集型还是IO密集型,多线程相对于多进程都没有绝对的性能优势。多线程相对于单线程的真正优势是降低平均响应时间。此外,进一步了解最常用的Reactor模型和多线程的一些细节上的东西。
多线程的主要适用场景是计算与IO叠加进行的场景,多进程的主要适用场景是各项任务较为独立的场景。没有银弹,选择多进程还是多线程,需要根据针对不同的场景做不同的设计。
进程与线程
进程与线程之间的区别是老生常谈的知识点,也是我们在设计一个系统时首先要考虑的点。今年阅读和编写了很多多进程和多线程的代码,也看了很多多线程和多进程编程的文章。个人对这两个东西的理解有了很大的提升。下面叙述最近醒悟的几个点:
① 之前我一直以为UI应用就应该使用多线程模型,其实大错特错。
的确,UI应用一般会有一个UI线程用来事件循环界面上的事件,但是其他脏活累活既可以扔给其他线程、也可以扔给其他进程,前者需要考虑线程之间的逻辑关系,而后者考虑的是进程之间的逻辑关系。
② 如果某个多任务应用需要适应单核环境,那么这时候应该使用状态机还是多线程?
单核环境下,状态机的好处是手动调度可以获取更好的性能,但是协调各个任务之间的关系略显蛋疼。相对来说,多线程更加直观,但是线程在单核上的上下文切换以及同步就比较糟蹋性能了。所以说,具体问题具体分析,特殊情况特殊对待,设计系统这种东西,要看场景,要讲经验。
③ 多核环境下呢?
多核环境下,状态机和多线程,当然首选多线程,这样可以更好的发挥多核处理器的性能。那么既然是多核了,直接上多进程来替代多线程岂不更好?问题来了,单核环境下多线程没有状态机高效,多核环境下多线程没有多进程高效,那么多线程的真正优势是什么?
③ 多线程的真正优势
多线程相对于单进程的真正优势是降低响应时间,提高响应速度。但是,多线程与多进程之间的选择往往视情况而定,这里引用书中原话“在其他条件相同的情况下,可以根据工作集合(work set)的大小来取舍,工作集指服务程序响应一次请求所访问的内存大小。如果工作集较大,那么就用多线程,避免CPU cache换入换出,影响性能;否则,就用单线程多进程,享受单线程编程的遍历。”
即,多线程和多进程的共享内存同步和通信都比较麻烦,但是如果工作集较大,则多线程避免cpu cached换入换出。但是如果用不了什么内存的话,再看任务的独立性,任务之间较为独立,则可以划归为多进程。其实还是要具体问题具体分析,不能一概而论。实际上,进程往往是我们用来划分系统功能的重要决策点。
所以多线程现对于多进程的优势就是,在工作集较大的情况下,性能较为可观,可以避免每个进程自己保留一份cache。
单线程服务器常用编程模型
单线程服务器编程模型中最常用的是 non-blocking IO + IO multiplexing ,就是Reactor模型。
常用到什么程度呢?lighttpd你知道吧,单线程服务器。Nginx你知道吧,每个工作进程有个事件循环,一样的道理。ACE这种烂成渣的东西也实现了这种模型。还有Java NIO,顾名思义,Non-blocking IO,一看就知道是个啥玩意。还有python的一个网络库叫Twist,有个人之前还推荐过我用来着。
可以说Reactor模型非常常见,而且主要是好写、好维护,效率也还阔以。不像Proactor模型,一波异步之后,代码就支离破碎、不堪入目,当然了如果一波封装之后,也还好,就是好的异步调用设计方案太难得了,我自己在现有项目中设计过一套,但是只能说凑合用吧,并不是特别满意,还有很多待改进空间,话扯远了。
多线程服务器常用编程模型
多线程服务器编程模型中有如下几种:
- 每个请求创建一个线程,每个线程阻塞式IO,即one thread per request。缺点可想而知,高并发一波就能搞垮机器,伸缩性不好
- 在1的基础上,使用线程池,稍稍提高性能
- non-blocking IO + IO multiplexing,其实还是Reactor模型。只不过扩展到多线程模型中变成 non-blocking IO + one loop per thread,即每个线程有个event loop
- Leader/Follower 等高级模式,该模式书中未详细记载,暂且放一放不去研究
一般使用第三种,该模式的使用逻辑是,需要哪个线程干活,就把timer或者IO channel注册到哪个线程的loop中去。比如说,对实时性要求高的connection可以单独用一个线程,数据量大的connection可以独占一个线程,并把数据处理任务分其他计算线程中,次要辅助的connections可以共用一个线程。
推荐模式
作者陈硕在这本书中推荐道,C++多线程服务器端编程模式为:one (event) loop per thread + thread pool。即两点:
- event loop(也叫 IO loop)用作IO multiplexing,配合non-blocking IO和定时器
- thread pool用来做计算,具体可以是任务队列或者生产者消费者队列用于获取其他线程压入的数据
进程间通信只用TCP
进程间通信有许多方式:管道(匿名和具名)、POSIX消息队列、共享内存、信号,还有Sockets。同步原语有互斥器、条件变量、信号量等等。作者在书中力推进程间通信使用TCP,好处有以下几点:
- 伸缩性强、跨主机
- TCP链接天然就是双向通信(管道就仅仅是单向)
- TCP socket是文件描述符,进程结束时操作系统会关闭所有文件描述符,所以即使程序退出,系统回收垃圾。相反,跨进程的mutex如果不能在程序中正常销毁,那么就不得不重启。
- 两个进程间通信,如果一个崩溃了,另一个进程会立刻感知。
- 使用wireshark等抓包工具可以记录或者重现问题。可以借此编写回归测试和压力测试
分布式系统中使用TCP长连接通信
使用长连接通信好处有二:
- 容易定位分布式服务系统中服务之间的依赖关系。比如使用
netstat -tpna | grep :port
,可以列出用到某个服务的客户端地址,然后在客户端机器上用netstat或lsof定位是客户端上的哪个进程 - 通过接收和发送队列的长度来定位网络或程序故障。如果Send-Q保持不变或者持续增加,那通常意味着服务进程处理速度变慢,可能发生死锁或阻塞。如果Send-Q保持不变或者增加,有可能是对面服务器太忙、来不及处理,也有可能是网络故障,甚至是服务器掉线。
多线程编程的适用场景
多线程的适用场景是:提高响应速度,让IO和计算相互重叠,降低延迟。多线程无法提高绝对性能,但是能提高平均响应性能。一个多线程服务端程序中的线程大致有三类:
- IO线程,这类线程主循环是IO multiplexing
- 计算线程,这类线程主循环是blocking queue
- 第三方库所用线程,比如logging,database connection等等
没有帐号?立即注册