声明:以下内容若无特别说明,均指Linux服务器环境下,传输层协议为TCP、主要开发语言为C++。
开发服务器端程序最基础的工作就是处理并发连接,服务器端网络编程处理并发连接主要有以下两种方式:
- 当线程廉价时,一台机器上可以创建远多于机器CPU物理线程数的“线程”,这是一个线程只处理一个TCP连接,通常使用阻塞IO(至少看起来如此)。例如Go goroutine、Erlang actor。这里的线程由语言runtime调用,与操作系统的线程不是一回事。
- 当线程很宝贵时,一台机器上只能创建与机器CPU数目相当的线程。这是,一个线程处理多个 TCP连接上的IO,通常使用非阻塞IO和IO多路复用。例如libevent、boost.asio。这是原生线程,能被操作系统的任务调度器看见。
对于C++服务器端开发,只考虑后一种方式。
首先,一个有多台机器组成的分布式系统必然是多进程的,因为进程不能跨操作系统边界。在这个前提下,我们把目标集中到一台机器,一台至少拥有4核心的普通服务器。如果要在一台多核服务器上提供一个服务,可用的模式有:
- 运行一个单线程的进程:模式1是不可伸缩的(scalable),不能发挥多核机器的计算能力。
- 运行一个多线程的进程:模式2难写。
- 运行多个单线程的进程:a)简单地把模式1的进程运行多份,b)主进程+worker进程,如果必须要绑定到一个TCP port。
- 运行多个多线程的进程:模式4不但没有结合2和3的优点,反而结合了2和3的缺点。
重点讨论2和3b)的优劣,即:什么时候一个服务器程序应该是多线程(模式2)的?从功能上讲,没有什么是多线程能做到而单线程做不到的,反之亦然。从性能上讲,无论是IO bound还是CPU bound的服务,多线程都没有什么优势。如以下情况
1.单线程的适用场景
一般有两种场景必须适用单线程;
- 程序可能会fork():如集群中在计算节点上负责启动任务的的的守护进程(即“看门狗”程序)。
- 限制CPU占用率:例如一个8核的服务器上,一个单线程的程序即便发生busy-wait,占满一个核心,其CPU使用率也只有12.5%。最坏情况下仍有87.5%的计算资源可用。
从编程的角度上说,单线程优势就是:简单。程序的结构一般是一个IO多路复用的event loop,直接使用阻塞IO。Event loop有一个明显的缺点:它是非抢占的,耗时短且优先级高的的task可能需要等到的耗时长且优先级低的task完成后才能进行,相当于发生了优先级反转。前面我说,无论是IO bound还是CPU bound,多线程都没有什么绝对意义上的优势。这句话的意思是,如果用很少的CPU负载就能让IO跑满,或者用很少的IO流量就能让CPU跑满,那么多线程没啥用。前者例子很多,如静态web服务器,或者FTP服务器,CPU的负载很轻,主要瓶颈在网络IO和磁盘IO。这个时候一个单线程程序就能撑满IO。增加CPU的数目也不能提升吞吐量。CPU跑满的情况较少见,如subset sun问题,对于这种情况3a)最适合,发挥多核优势。
也就是说,如果一方早早地先到达的瓶颈,多线程程序都没什么优势。
2.多线程的适用场景
多线程的适用场景是:提高响应速度,让IO和计算相互重叠,降低延时。虽然多线程不能提高绝对性能,但是可以提高平均响应性能。一个程序要做成多线程程序,大致要满足:
- 有多个CPU可用。单核上多线程没有性能优势(但是或许可简化并发业务逻辑的实现)。
- 多线程可有效地划分责任和功能模块,让每一个线程的逻辑简单,任务单一,便于编码。而不是把所有逻辑塞到一个event loop中,不同类别的事件之间相互影响。
- 线程中有共享数据,即内存的全局状态。但是应该尽量减少线程间的共享数据。
- 共享的数据可以修改,不是静态的。如果数据是静态的,利用进程间shared memory,模式3即可。
- 事件的响应有优先级。可以用专门的线程来处理优先级高的事件。
- 时延和吞吐量同样重要,不是简单的IO bound或CPU bound程序。
- 利用异步操作。无论往磁盘写log file,还是往log server发消息都不该阻塞关键路径。
- 具有可预测的性能。随着负载增加,性能缓慢下降,超过某个临界点后计算下降。线程数目不随负载变化。
例子暂略。
线程的分类
一个多线程服务程序中大致可以分为3类:
- IO线程。这类线程的主循环是IO多路复用,阻塞地等在select/poll/epoll_wait系统调用上。这类线程也处理定时时间。当然也不全是IO处理,一些简单的计算也可以放入其中,如消息的编码解码。
- 计算线程,这类线程的主循环是阻塞队列,阻塞地等在条件变量上。这类线程一般在线程池中。这种线程通常不涉及IO,一般要避免任何阻塞操作。
- 第三方库所用的线程,比如logging,又比如database connection。
服务器端程序一般不会频繁启动或终止线程。