常见多线程与并发服务器模型

一、常见并发服务器方案

在这里插入图片描述
在这里插入图片描述

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

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

2.concurrent(并发式)服务器

one connection per process/one connection per thread

在这里插入图片描述
one connection per process: 主进程每次fork 之后要关闭connfd,子进程要关闭listenfd

one connection per thread : 主线程每次accept 回来就创建一个子线程服务,由于线程共享文件描述符,故不用关闭。

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

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

这种服务器由于多个进程在accept等待中,当一个请求到达时,都会被触发,但是只有一个成功返回,这就是“惊群”现象。
如今网络编程中经常用到多进程或多线程模型,大概的思路是父进程创建socket,bind、listen后,通过fork创建多个子进程,每个子进程继承了父进程的socket,调用accpet开始监听等待网络连接。这个时候有多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就是“惊群”。这样会导致什么问题呢?我们知道进程被唤醒,需要进行内核重新调度,这样每个进程同时去响应这一个事件,而最终只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠或其他。

在Nginx中,accept是交给epoll机制来处理的,Nginx中使用mutex互斥锁解决这个问题,具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。

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

在这里插入图片描述
并发处理多个请求,是在一个线程中完成的,无法充分利用多核cpu,实际上是单线程轮询多个客户端。利用I/O多路复用(select、poll、epoll)实现,无法充分利用多核CPU。网络编程中的select、poll、epoll不适合执行时间比较长的服务,所以为了让客户感觉是在“并发”处理而不是“循环”处理,每个请求必须在相对较短的时间内执行完毕。

5.reactor+thread per request

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

6.reactor+worker thread

7、reactor + thread pool(能适应密集计算)

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

处理完成之后,线程池中的线程并不负责数据的发送,要响应数据包,还必须要丢到Reactor线程中来发送或者通过异步调用Reactor线程中的发送函数来发送。

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与密集计算)

在这里插入图片描述
这也是muduo库推荐的模型。

这里的multiple reactors只能用线程来实现,不能用进程,因为进程无法共享线程池。因此,多个subReactor线程可以共享线程池。
每个subReactor都把各自的请求包丢到线程池中处理(和方案七一样)。
多个subReactor共享一个线程池,其实就相当于两个线程池(I/O线程池+计算线程池Threadpoll)。subReactor可以有多个,但threadpool只有一个。

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

上面的九种模型都是基于同步I/O的,现在来看异步I/O:
在这里插入图片描述
也就是说I/O操作在执行的时候,other processing也在执行。异步I/O是不会阻塞的,那么它与同步I/O的非阻塞模式有什么区别呢?同步I/O的非阻塞模式只是将文件描述符增加属性O_NONBLOCK,然后调用read函数来接收,如果数据没有准备好,那么read函数也会立即返回,不会阻塞,而如果我们想要得到数据,就需要循环调用read函数(即处于忙等待的状态)。
再来分析异步I/O与I/O复用的区别:I/O复用仅仅只是得到通知有I/O操作完成(内核中有数据了),仍然需要调用read函数来接收数据(即把数据从内核缓冲区拉倒用户缓冲区),而异步I/O会在I/O操作完成后主动把数据从内核缓冲区推到用户缓冲区。
异步I/O能够让I/O操作与计算重叠。充分利用硬件的DMA特性。

Linux异步IO
glibc版本,有bug。

int aio_read(struct aiocb *aiocbp);

linux native aio,也不完美。目前仅支持 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个线程并发压缩一个文件)”,这两种方式完成100个文件的压缩总耗时相当,但是第二种方式能较快的拿到第一个压缩完的文件。

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(IO操作不耗CPU),那么T=16,也就是16个“50%繁忙的线程”能让8个CPU忙个不停。

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值