文章目录
并发服务器的历史线:
只启动一个线程,线程处理数据用阻塞的方式
这样的话会非常浪费服务器的资源。根本不叫做并发,同时只能服务一个客户端。充其量 CPU 每秒进行多次任务切换,task switching,任务切换就是并发的假象。
只启动一个线程,线程处理数据用非阻塞的方式
使用 fork 启动多个进程,进程处理数据使用非阻塞的方式
启动多个线程,线程处理数据用非阻塞的方法
但是线程的切换需要资源,如果对实时性有要求的话,不能容忍多线程切换。
创建子进程池,处理数据需要的子进程将从进程池当中取出
使用线程池来限制线程切换的数量,线程处理数据用无阻塞的方式。
但是线程池里,切换线程还是慢
使用 IO 多路复用,线程处理数据使用无阻塞的方式。(Reactor 模式)
通过使用epoll来实现 IO 多路复用。这里推荐使用 libevent。但是没有用到多线程,无法充分发挥多核 CPU 的优势。
reactor + worker thread(过渡方案)
用 reactor 来监听listenfd描述符,当accept()函数返回时候,然后启动线程,也就是为每一个连接创建一个线程,在线程中对 socket 进行读写。
reactor + thread pool(过渡方案)
先创建好线程池,用 reactor 来监听listenfd描述符,当accept()函数返回时候,然后从线程池当中取出线程,在线程当中对 socket 进行读写。
reactors in threads(one loop per thread)
对 socket 描述符的 read 和 write 也使用 reactor 模式
在 reactor + thread pool 当中,我们仅仅针对 listenfd 使用了 reactor 模式,而对accept()得到的描述符是直接使用阻塞的方式。而这里再加上让 reactor 模式监听 socket 描述符。意思就是说在主线程当中用 reactor 监听 listenfd 描述符,在副线程中用 reactor 监听accept()得到的描述符。
multiple reactors + thread pool(one loop per thread + thread pool)
启动一个线程池,分一半的线程给 reactor,另外一半的线程做其他计算。
使用 IO 多路复用,异步读写,线程处理数据使用无阻塞的方式。(Preactor 模式)
boost::asio 是 preactor 的库。但是它是使用epoll来模拟preactor模式的。
在我的文章《socket 连接池 SocketPool 分析 (三):《UNPv1》复习(下):IO 多路复用》中也提到过了。
使用epoll还是同步 IO,要使用异步 IO 的话,就要在 linux 下使用 AIO 系统调用了。请参看知乎上陈硕老师的回答:怎样理解阻塞非阻塞与同步异步的区别?
在 linux 下还是首选 reactor 模式。
常见问题:
linux 能同时启动多少线程?
答:对于 32-bit Linux,一个进程的地址空间是 4G,其中用户态能访问 3G 左右,而一个线程的默认栈 (stack) 大小是 8M,心算可知,一个进程大约最多能同时启动 350 个线程左右。
多线程能提高并发度吗?
答:如果指的是 “并发连接数”,不能。
假如单纯采用 thread per connection 的模型,那么并发连接数大约 350,这远远低于基于事件的单线程程序所能轻松达到的并发连接数(几千上万,甚至几万)。所谓 “基于事件”,指的是用 IO multiplexing event loop 的编程模型,又称 Reactor 模式。
多线程能提高吞吐量吗?
答:对于计算密集型服务,不能。
如果要在一个 8 核的机器上压缩 100 个 1G 的文本文件,每个 core 的处理能力为 200MB/s,那么 “每次起 8 个进程,一个进程压缩一个文件” 与 “只启动一个进程(8 个线程并发压缩一个文件)”,这两种方式总耗时相当,但是第二种方式能较快的拿到第一个压缩完的文件。
多线程如何让 I/O 和计算重叠
答:多线程程序如何让 I/O 和计算重叠,降低 latency(迟延)
例:日志(logging),多个线程写日志,由于文件操作比较慢,服务线程会等在 IO 上,让 CPU 空闲,增加响应时间。
解决办法:单独用一个 logging 线程负责写磁盘文件,通过 BlockingQueue 提供对外接口,别的线程要写日志的时候往队列一塞就行,这样服务线程的计算和 logging 线程的磁盘 IO 就可以重叠。
如果异步 I/O 成熟的话,可以用 protator 模式。
线程池大小的选择
如果池中执行任务时,密集计算所占时间比重为 P(0
假设 C=8,P=1.0,线程池的任务完全密集计算,只要 8 个活动线程就能让 CPU 饱和
假设 C=8,P=0.5,线程池的任务有一半是计算,一半是 IO,那么 T=16,也就是 16 个 “50% 繁忙的线程” 能让 8 个 CPU 忙个不停。
线程分类
I/O 线程(这里特指网络 I/O)
计算线程
第三方库所用线程,如 logging,又比如 database