为了更加形象的说明同步/异步
、阻塞/非阻塞
,我们以小明去买奶茶为例。
- 同步:当一个
同步调用
发出去后,调用者
要一直等待调用结果的通知
,直到得到调用结果
。 - 异步:当一个
异步调用
发出去后,调用者
不能立即得到调用结果的返回
。
对于异步调用
,要想获得结果,一般有2种
方式:
- 主动
轮询
异步调用的结果 - 被调用方通过
callback
来通知调用方调用结果
举个栗子
- 同步买奶茶:小明点单交钱,然后等着拿奶茶。
- 异步买奶茶:小明点单交钱,店员给小明一个小票,等小明奶茶做好了再来取。
异步买奶茶,小明要想知道奶茶是否做好了,有2种
方式:
- 小明主动去问店员,一会就去问一下:“奶茶做好了吗?”...直到奶茶做好。
- 等奶茶做好了,店员喊一声:“小明,奶茶好了!”,然后小明去取奶茶。
总结:同步与异步
的重点在消息通知的方式
上,也就是调用结果通知的方式
。
阻塞调用
发出去后,在消息返回
之前,当前进/线程
会被挂起
,直到有消息返回
,当前进/线程
才会被激活
。非阻塞调用
发出去后,不会阻塞
当前进/线程
,而会立即返回
。
举个栗子
- 阻塞买奶茶:小明点单交钱,干等着拿奶茶,什么事都不做。
- 非阻塞买奶茶:小明点单交钱,等着拿奶茶,等的过程中,时不时刷刷微博、朋友圈
总结:同步与异步
重点在于消息通知的方式
,阻塞与非阻塞
重点在于等消息时候的行为
。
所以,就有了下面4种组合方式:
- 同步阻塞:小明在柜台干等着拿奶茶;
- 同步非阻塞:小明在柜台边刷微博边等着拿奶茶;
- 异步阻塞:小明拿着小票啥都不干,一直等着店员通知他拿奶茶;
- 异步非阻塞:小明拿着小票,刷着微博,等着店员通知他拿奶茶。
总结:阻塞与非阻塞
的重点在于进/线程等待消息时候的行为
,也就是在等待消息的时候,当前进/线程是挂起
状态,还是非挂起
状态。
Apache处理一个请求是同步阻塞的模式。如图:
每到达一个请求,Apache都会去fork一个子进程去处理这个请求,直到这个请求处理完毕。面对低并发,这种模式没什么缺点,但是,面对高并发,就是这种模式的软肋了。
1个客户端占用1个进程,那么,进程数量有多少,并发处理能力就有多少,但操作系统可以创建的进程数量是有限的。并且,多进程就会有进程间的切换问题,而进程间的切换调度势必会造成CPU的额外消耗。当进程数量达到成千上万的时候,进程间的切换就占了CPU大部分的时间片,而真正进程的执行反而占了CPU的一小部分,这就得不偿失了。
下面,举例说明这2种场景是多进程模式的软肋:
-
即时消息通知程序比如及时聊天程序,一台服务器可能要维持数十万的连接,那么就要启动数十万的进程来维持。这显然不可能。
-
调用外部Http接口时假设Apache启动100个进程来处理请求,每个请求消耗100ms,那么这100个进程能提供1000qps。但是,在我们调用外部Http接口时,比如QQ登录、微博登录,耗时较长,假设一个请求消耗10s,也就是1个进程1s处理0.1个请求,那么100个进程只能达到10qps,这样的处理能力就未免太差了。
传统的服务器模型
就是这样,因为其同步阻塞
的多进程模型
,无法面对高并发
。那么,有没有一种方式可以让我们在一个进程处理所有的并发I/O
呢?答案是有的,这就是I/O复用技术
。
所谓的I/O复用
,就是多个I/O可以复用一个进程
。上面说的同步阻塞
的多进程模型
不适合处理高并发,那么,我们再来考虑非阻塞
的方式。采用非阻塞
的模式,当一个连接过来时,我们不阻塞住,这样一个进程可以同时处理多个连接了。
比如一个进程接受了10000
个连接,这个进程每次从头到尾
的问一遍这10000
个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”
然后进程就一直从头到尾问这10000
个连接,如果这1000
个连接都没有I/O事件
,就会造成CPU的空转
,并且效率也很低
,不好不好。
于是伟大的程序猿们日思夜想的去解决这个问题...终于!
- 我们能不能引入一个
代理
,这个代理
可以同时观察许多I/O流事件
呢?
当没有I/O事件
的时候,这个进程处于阻塞状态
;当有I/O事件
的时候,这个代理
就去通知进程醒来
?
于是,早期的程序猿们发明了两个代理:select
、poll
。
select
、poll
代理的原理是这样的:
当连接有I/O流事件
产生的时候,就会去唤醒进程去处理
。但是进程并不知道是哪个连接产生的I/O流事件
,于是进程就挨个去问:“请问是你有事要处理吗?”......问了99999
遍,哦,原来是第100000
个进程有事要处理。那么,前面这99999
次就白问了,白白浪费宝贵的CPU时间片
了!痛哉,惜哉...
注:select
与poll
原理是一样的,只不过select
只能观察1024
个连接,poll
可以观察无限个
连接。
上面看了,select
、poll
因为不知道哪个连接有I/O流事件
要处理,性能也挺不好的。
那么,如果发明一个代理,每次能够知道哪个连接有了I/O流事件
,不就可以避免无意义的空转了吗?
于是,超级无敌、闪闪发光的epoll
被伟大的程序员发明出来了。
epoll
代理的原理是这样的:
当连接有I/O流事件
产生的时候,epoll
就会去告诉进程哪个连接有I/O流事件
产生,然后进程就去处理这个连接
IO复用异步非阻塞
程序使用经典的Reactor模型
,Reactor
顾名思义就是反应堆
的意思,它本身不处理任何数据收发
。只是可以监视一个socket(也可以是管道、eventfd、信号)句柄的事件变化。
句柄
英文为handler
,可以形象的比喻为锅柄、勺柄
。也就是资源的唯一标识符
、资源的ID
。通过这个ID可以操作资源。
Reactor
只是一个事件发生器
,实际对socket句柄
的操作,如connect/accept、send/recv、close是在callback中完成的
。
Swoole
的worker
进程有2种类型:一种是普通的worker进程
,一种是task worker进程
。
worker进程
:用来处理普通的耗时不是太长的请求task worker进程
:用来处理耗时较长的请求,比如数据库的I/O
操作
我们以异步MySQL
举例:
如此,通过worker、task worker
结合的方式,我们就实现了异步I/O
。