本章作者主要是总结了一两种常用的线程模型。归纳了进程通信与线程同步的最佳实践
进程与线程的区别:
- 进程是文件系统中的最重要的两个概念之一(令一个是文件)。简单地说,一个进程是内存中正在运行的程序。每个进程有自己独立的地址空间。
- 线程是依附在进程上面的,它的特点是共享地址空间,从而可以高效地共享数据。一台机器上的进程可以共享代码段,但是不能共享数据。如果多个进程大量共享内存,那就等于是把多进程当成多线程来写。
单线程服务器的常用编程模型
在高性能网络程序中,使用的最广泛的是“non-blocking IO + multiplexing IO”这种模型,即reactor模式。
在non-blocking IO + multiplexing IO 模型中,程序的主要结构是一个事件循环。以事件驱动和事件回调的方式实现业务逻辑。Reactor模型优点编写简单,效率不错,对于IO密集型应用是一个不错选择。但是它也有其本质的缺点:它要求事件的回调函数必须是非阻塞的,而且对于网络IO的请求相应协议,它容易割裂业务逻辑,使其散布与多个回调函数之间。
多线程服务器的常用编程模型
多线程的服务器编程模型主要有以下四种:
- 为每个请求创建(临时创建)一个线程,使用阻塞式IO操作。
- 使用线程池,同样使用阻塞式IO操作。相比于第一种,这是提高性能的措施。
- 使用non-blocking IO + IO multiplexing
- Leader/Follower等高级模型
muduo中主要采用的是第三种来编写多写成网络服务程序。即 non-blocking + one loop per thread。
non-blocking + one loop per thread
在这种模型下,程序里的每个IO线程都有一个event loop,用于处理读写事件。这种方式有以下好处:
- 线程数目固定,可以在启动的时候设置,不需要频繁创建和销毁
- 可以很方便在线程间调配负载(根据各个IO线程处理的IO事件多少来决定下一个IO事件由那个IO线程处理)
IO事件发生的线程是固定的,同一个TCP不需要考虑事件并发。
线程池
对于没有IO而光有计算任务的线程,使用event loop有点浪费。一般用blocking quene的方法来实现任务队列。除了任务队列,还可以用BlockingQuene实现数据的生产者消费者队列,即T是数据类型而不是函数类型。
推荐模式
muduo推荐的C++多线程服务器编程模式为:one (event) loop per thread + thread pool。其中
- event loop用作IOmultiplexing,配合non-blocking和定时器
- thread pool用来做计算,具体可以是任务队列或者生产者消费者队列
进程间通信只用TCP
进程间通信包含下面的模式:
- 匿名管道
- 命名管道
- POSIX消息队列
- 共享内存
- 信号
- socket
- ……..
进程间通信muduo主要选择socket。好处在于:
- 跨主机,具有伸缩性
- 容易debug
多线程服务器的适用场合
开发服务器程序的基本任务是处理并发连接,服务端处理网络并发连接主要有下面两种方式
- 当线程很廉价时,一台机器上可以创建远高于CPU数目的线程。这时一个线程只处理一条连接,通常使用阻塞IO
- 当线程很宝贵时,一台机器只能创建与CPU数目相当的线程,这时一个线程要处理多条连接,通常使用非阻塞IO
为了充分利用硬件资源,本书主要采用后一种方式。下面需要解决的问题是:多个线程是应该属于一个进程,还是分属于多个进程?
首先,如果要在一台机器上提供一种服务或者执行一个任务,可用的模式有:
- 运行一个单线程的进程
- 运行一个多线程的进程
- 运行多个单线程的进程
- 运行多个多线程的进程
可以对这些模式做以下总结:
- 模式1是不可伸缩的,不能发挥多核的计算能力
模式3是公认的主流模式,它有以下两个子模式
- 3a 简单地把模式1种的进程运行多份
- 3b 主进程 + woker进程
模式2 是被很多人鄙视的,因为多线程程序难写,而且与3相比没有什么优势
- 模式4更是千夫所指,它汇聚了2和3的缺点。
下面主要讨论模式3和模式2的优劣
问题主要是:什么时候一个服务器程序应该用多线程编写??
必须使用单线程的场合
- 程序会调用fork
- 限制程序的CPU占有率
只有单线程的程序才能fork。而且单线程程序能限制CPU的占有率(无论如何,它都只占用一个core,如果多线程的话,可能会把多个核都占满),避免过分抢占系统资源。
单线程的优缺点
单线程程序编程简单,但是由于eventloop的非抢占式,单线程可能会造成优先级反转(P70)。这个缺点可以由多线程来克服。
其实在性能上,多线程没有绝对的优势。因为如果很少的CPU负载能让IO慢跑,那多线程是没啥用处的,而且即使是在CPU满跑的情况下,选用模式3a应该是最合适的。
综上,多线程几乎没有什么优势
适用于多线程的场景
- 有多个CPU可用
- 线程间有共享数据
- 线程间的共享数据可以修改
- 提供非匀质服务,也就是事件的相应有优先级差异,可以用专门的线程来处理优先级高的事件(这就是解决前面单线程会遇到的优先级反转问题)。
- 程序要有相当的计算量
- 利用异步操作
- 能scale up。能享受增加CPU的好处
一个多线程程序中线程主要分为以下几类
- IO线程。主循环是IO multiplexing,阻塞在epoll_wait/select/poll系统调用上。该线程也处理定时器事件。当然一些简单的计算也可以放进去
- 计算线程。主循环是blocking quene,阻塞在condition variable的等待上。
- 第三方库所用的线程。
多线程和单线程多进程的取舍
在其他条件相同的情况下,可以根据工作集的大小来选择
工作集:服务程序响应一次请求所访问的内存大小。
如果工作集较大,那么就用多线程,避免CPU cache换入换出,影响性能。否则就用单线程多进程,享受单线程编程的便利。