多线程与并发服务器设计

常见并发服务器方案

1.iterative(循环式/迭代式)服务器


    iterative 只能使用短连接(每处理完一个连接,然后就关闭连接,称为短连接),不能使用长连接,如果使用长连接,意味着write需要转到read,那么整个程序就是一个单线程程序,如果此时有其它线程过来,没有办法接受连接,因为前一个线程还在read->write的循环中。也就是说如果使用长连接的话,这个程序只能够处理一个客户端,而不能处理多个客户端。要想让程序处理多个客户端,只能使用短连接。这种服务器不是真正意义上的并发服务器,它是循环的,称为循环服务器。 

这种服务器有很多缺陷 : 
单线程,不能充分利用多核cpu
因为是短链接,有可能上一次断开的连接就是本次的连接,这样效率明显下降

不能并发处理

2.concurrent(并发式)服务器


这种服务器,因为是多线程或者多进程的,所以能并发处理请求。
注:如果是多进程,记得关闭监听套接字,因为子进程会继承套接字。

如果是多线程,就不用关闭监听套接字了,因为线程没有继承打开的套接字。

3.prefork or threaded服务器(预先创建进程和线程)


创建套接字、绑定、监听,预先创建若干个子进程,子进程负责与客户端的通信。原理和并发式服务器类似。不同是预先创建进程或线程,减少了开销。能够提高响应速度。这里有一个问题,当一个客户端连接过来的时候,会有惊群的可能性。

这种服务器由于多个进程在accept等待中,当一个请求到达时,都会被触发,但是只有一个成功返回。这是一种“惊群”现象。

4.reactive(反应式)服务器(使用的是reactor模式)


并发处理多个请求,是在一个线程中完成。实际上单线程轮询多个客户端。 利用I/O复用(select、poll、epoll)实现。无法充分利用多核CPU,网络编程中的select

不适合执行时间比较长的服务,所以为了让客户感觉是在“并发”处理而不是“循环”处理,每个请求必须在相对较短时间内执行

5.reactor+thread per request

每个请求过来,创建一个线程。这样就能利用多CPU

6.reactor+worker thread

7.reactor+thread pool(第5种方案的改进)


一个客户端来连接,并且发送请求包过来,在Reactor中(这是一个线程)读取请求包,并这个请求包丢到线程池中处理,线程池会取出工作线程对其进行处理。这是即使计算量较大,时间较长,也没有关系,因为是线程池中的线程进行处理的,不会影响到Reactor这个I/O线程,还可以接受其它客户端的连接,所以能够计算密集型任务。

处理完成之后,线程池中的线程并不负责数据的发送,要响应数据包,还必须丢到I/O线程中来发送或者异步地调用I/O线程的发送方法来发送

8.multiple reactors(能适应更大的突发I/O)


多个事件循环,每个reactor都是一个线程或者一个进程。
reactors in threads(one loop per thread)
每个线程都有一个reactor,也就是事件循环
reactors in process
每个进程都有一个reactor

每一个reactor都是一个线程(或进程),将监听套接字加入mainReactor,每当客户端连接过来时,监听套接字产生可读事件,acceptor就返回已连接套接字,把返回的已连接套接字加入subReactor里,用它来处理客户端的连接,如果再来一个客户端,就按顺序的分配到下一个subReactor,当第三个客户端连接的时候又回到了第一个subReactor。这种方式称为round robin(轮叫),它能够保证subReactors中处理的事件是均匀的,而不至于每一个事件循环处理的连接过多。每一个连接只能在一个subReactor中处理,而不能在一个subReactor中read,在另一个subReactor中send
这种方式能够适应突发的I/O请求

9.multiple reactors+thread pool(one loop per thread+threadpool)(突发I/O与密集计算)


这里的multiple reactors只能用线程来实现,不能用进程,否则后面的线程池无法共享
多个subReactor共享一个线程池,其实就相当于两个线程池(I/O线程池+计算线程池Threadpoll)

10.proactor服务器(proactor模式,基于异步I/O)

前面介绍的服务器都是基于同步I/O,异步I/O使得I/O操作和其他操作能够重叠,I/O操作的时候,计算操作也在同时执行
理论上proactor比reactor效率要高一些
异步I/O能够让I/O操作与计算重叠。充分利用DMA特性
Linux异步IO
glibc aio(aio_*),有bug
kernel native aio(io_*),也不完美。目前仅支持 O_DIRECT 方式来对磁盘读写,跳过系统缓存。要自已实现缓存,难度不小。

boost asio实现的proactor,实际上不是真正意义上的异步I/O,底层是用epoll来实现的,模拟异步I/O的。

常见并发服务器方案比较


二、一些常见问题

1、Linux能同时启动多少个线程?
对于 32-bit Linux,一个进程的地址空间是 4G,其中用户态能访问 3G 左右,而一个线程的默认栈 (stack) 大小是 8M,心算可知,一个进程大约最多能同时启动 350 个线程左右。
2、多线程能提高并发度吗?
如果指的是“并发连接数”,不能。

假如单纯采用 thread per connection 的模型,那么并发连接数大约350,这远远低于基于事件的单线程程序所能轻松达到的并发连接数(几千上万,甚至几万)。所谓“基于事件”,指的是用 IO multiplexing event loop 的编程模型,又称 Reactor 模式。

3、多线程能提高吞吐量吗?
对于计算密集型服务,不能。

如果要在一个8核的机器上压缩100个1G的文本文件,每个core的处理能力为200MB/s,那么“每次起8个进程,一个进程压缩一个文件”与“只启动一个进程(8个线程并发压缩一个文件)”,这两种方式总耗时相当,但是第二种方式能较快的拿到第一个压缩完的文件。

4、多线程能提高响应时间吗?
可以。参考问题3

5、多线程程序日志库要求
线程安全,即多个线程可以并发写日志,两个线程的日志消息不会出现交织。
用一个全局的mutex保护IO
每个线程单独写一个日志文件
前者造成全部线程抢占一个锁(串行写入)
后者有可能让业务线程阻塞在写磁盘操作上。(磁盘IO时间比较长)

解决办法:用一个logging线程负责收集日志消息,并写入日志文件,其他业务线程只管往这个“日志线程”发送日志消息(如通过BlockingQueue提供接口),这称为“异步日志”,也是一个经典的生产者消费者模型。

6、线程池大小的选择
如果池中执行任务时,密集计算所占时间比重为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忙个不停。

7、线程分类
I/O线程(这里特指网络I/O)
计算线程
第三方库所用线程,如logging,又比如database

猜你喜欢

转载自blog.csdn.net/wk_bjut_edu_cn/article/details/80848872