文章目录
  1. 1. 只启动一个线程,线程处理数据用阻塞的方式
  2. 2. 只启动一个线程,线程处理数据用非阻塞的方式
  3. 3. 使用fork启动多个进程,进程处理数据使用非阻塞的方式
  4. 4. 启动多个线程,线程处理数据用非阻塞的方法
  5. 5. 创建子进程池,处理数据需要的子进程将从进程池当中取出
  6. 6. 使用线程池来限制线程切换的数量,线程处理数据用无阻塞的方式。
  7. 7. 使用IO多路复用,线程处理数据使用无阻塞的方式。(Reactor模式)
  8. 8. reactor + worker thread(过渡方案)
  9. 9. reactor + thread pool(过渡方案)
  10. 10. reactors in threads(one loop per thread)
  11. 11. multiple reactors + thread pool (one loop per thread + thread pool)
  12. 12. 使用IO多路复用,异步读写,线程处理数据使用无阻塞的方式。(Preactor模式)
  13. 13. 常见问题:
    1. 13.1. linux能同时启动多少线程?
    2. 13.2. 多线程能提高并发度吗?
    3. 13.3. 多线程能提高吞吐量吗?
    4. 13.4. 多线程如何让I/O和计算重叠
    5. 13.5. 线程池大小的选择
    6. 13.6. 线程分类

这篇博客是根据这篇文章常见多线程与并发服务器设计方案举例整理的。

并发服务器的历史线:

只启动一个线程,线程处理数据用阻塞的方式

这样的话会非常浪费服务器的资源。根本不叫做并发,同时只能服务一个客户端。充其量CPU每秒进行多次任务切换,task switching,任务切换就是并发的假象。
循环式服务器

只启动一个线程,线程处理数据用非阻塞的方式

使用fork启动多个进程,进程处理数据使用非阻塞的方式

fork型服务器

启动多个线程,线程处理数据用非阻塞的方法

但是线程的切换需要资源,如果对实时性有要求的话,不能容忍多线程切换。

创建子进程池,处理数据需要的子进程将从进程池当中取出

进程池

使用线程池来限制线程切换的数量,线程处理数据用无阻塞的方式。

但是线程池里,切换线程还是慢

使用IO多路复用,线程处理数据使用无阻塞的方式。(Reactor模式)

通过使用epoll来实现IO多路复用。这里推荐使用libevent。但是没有用到多线程,无法充分发挥多核CPU的优势。
reactor

reactor + worker thread(过渡方案)

用reactor来监听listenfd描述符,当 accept()函数返回时候,然后启动线程,也就是为每一个连接创建一个线程,在线程中对socket进行读写。

reactor + thread pool(过渡方案)

先创建好线程池,用reactor来监听listenfd描述符,当 accept()函数返回时候,然后从线程池当中取出线程,在线程当中对socket进行读写。
reactor + thread pool

reactors in threads(one loop per thread)

对socket描述符的read和write也使用reactor模式
在reactor + thread pool当中,我们仅仅针对listenfd使用了reactor模式,而对accept()得到的描述符是直接使用阻塞的方式。而这里再加上让reactor模式监听socket描述符。意思就是说在主线程当中用reactor监听listenfd描述符,在副线程中用reactor监听accept()得到的描述符。

one loop per thread

multiple reactors + thread pool (one loop per thread + thread pool)

启动一个线程池,分一半的线程给reactor,另外一半的线程做其他计算。

one loop per thread + thread pool

使用IO多路复用,异步读写,线程处理数据使用无阻塞的方式。(Preactor模式)

boost::asio是preactor的库。但是它是使用epoll来模拟preactor模式的。
在我的文章《socket连接池SocketPool分析(三):《UNPv1》复习(下):IO多路复用》中也提到过了。

使用epoll还是同步IO,要使用异步IO的话,就要在linux下使用AIO系统调用了。请参看知乎上陈硕老师的回答:怎样理解阻塞非阻塞与同步异步的区别?

更多有关reactor和preactor模式的比较请参考Comparing Two High-Performance I/O Design Patterns这篇英文文献。

在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<P<=1),而系统一共有C个CPU,为了让C个CPU跑满而不过载,线程池大小的经验公式T=C/P,即T*P=C(让CPU刚好跑满 )
假设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

文章目录
  1. 1. 只启动一个线程,线程处理数据用阻塞的方式
  2. 2. 只启动一个线程,线程处理数据用非阻塞的方式
  3. 3. 使用fork启动多个进程,进程处理数据使用非阻塞的方式
  4. 4. 启动多个线程,线程处理数据用非阻塞的方法
  5. 5. 创建子进程池,处理数据需要的子进程将从进程池当中取出
  6. 6. 使用线程池来限制线程切换的数量,线程处理数据用无阻塞的方式。
  7. 7. 使用IO多路复用,线程处理数据使用无阻塞的方式。(Reactor模式)
  8. 8. reactor + worker thread(过渡方案)
  9. 9. reactor + thread pool(过渡方案)
  10. 10. reactors in threads(one loop per thread)
  11. 11. multiple reactors + thread pool (one loop per thread + thread pool)
  12. 12. 使用IO多路复用,异步读写,线程处理数据使用无阻塞的方式。(Preactor模式)
  13. 13. 常见问题:
    1. 13.1. linux能同时启动多少线程?
    2. 13.2. 多线程能提高并发度吗?
    3. 13.3. 多线程能提高吞吐量吗?
    4. 13.4. 多线程如何让I/O和计算重叠
    5. 13.5. 线程池大小的选择
    6. 13.6. 线程分类