1. 阅前热身
为了更加形象的说明同步异步、阻塞非阻塞,我们以小明去买奶茶为例。
1.1 同步与异步
同步与异步的理解
同步与异步的重点在消息通知的方式上,也就是调用结果通知的方式。 同步
: 当一个同步调用发出去后,调用者要一直等待调用结果的通知后,才能进行后续的执行。 异步
:当一个异步调用发出去后,调用者不能立即得到调用结果的返回。
异步调用,要想获得结果,一般有两种方式:
- 主动轮询异步调用的结果;
- 被调用方通过callback来通知调用方调用结果。
生活中的例子
同步买奶茶
:小明点单交钱,然后等着拿奶茶;异步买奶茶:小明点单交钱,店员给小明一个小票,等小明奶茶做好了,再来取。
异步买奶茶
: 小明要想知道奶茶是否做好了,有两种方式:
- 小明主动去问店员,一会就去问一下:“奶茶做好了吗?”…直到奶茶做好。这叫轮训。
- 等奶茶做好了,店员喊一声:“小明,奶茶好了!”,然后小明去取奶茶。这叫回调。
1.2 阻塞与非阻塞
阻塞与非阻塞的理解
阻塞与非阻塞的重点在于进/线程等待消息时候的行为,也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。
阻塞调用
在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活.
非阻塞
调用在发出去后,不会阻塞当前进/线程,而会立即返回。
生活中的例子
阻塞买奶茶
:小明点单交钱,干等着拿奶茶,什么事都不做; 非阻塞买奶茶
:小明点单交钱,等着拿奶茶,等的过程中,时不时刷刷微博、朋友圈。
1.3 总结
通过上面的分析,我们可以得知:
- 同步与异步,重点在于消息通知的方式;
- 阻塞与非阻塞,重点在于等消息时候的行为。
所以,就有了下面4种组合方式:
- 同步阻塞:小明在柜台干等着拿奶茶;
- 同步非阻塞:小明在柜台边刷微博边等着拿奶茶;
- 异步阻塞:小明拿着小票啥都不干,一直等着店员通知他拿奶茶;
- 异步非阻塞:小明拿着小票,刷着微博,等着店员通知他拿奶茶。
2. IO 复用
IO 复用例子说明
假设你是一个机场的空管,你需要管理到你机场的所有的航线, 包括进港,出港,有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。
你会怎么做?
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
那么问题就来了:
很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。空管员之间需要协调,屋子里面就1,2个人的时候还好,几十号人以后 ,基本上就成菜市场了。
空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。
现实上我们的空管同时管几十架飞机稀松平常的事情:
他们怎么做的呢?这个东西叫flight progress strip
。
每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。
这个东西现在还没有淘汰哦,只是变成电子的了而已。
是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
如果你把每一个航线当成一个Sock(I/O 流),空管当成你的服务端Sock管理代码的话.
第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)
其实I/O多路复用
这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing.
重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。
是不是听起来好拗口,看个图就懂了:
在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流,
最初级的I/O复用
所谓的I/O复用,就是多个I/O可以复用一个进程。
采用非阻塞的模式,当一个连接过来时,我们不阻塞住,这样一个进程可以同时处理多个连接了。
比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”
然后进程就一直从头到尾问这10000个连接,如果这1000个连接都没有I/O事件,就会造成CPU的空转,并且效率也很低,不好不好。
升级版的I/O复用
上面虽然实现了基础版的I/O复用,但是效率太低了。于是伟大的程序猿们日思夜想的去解决这个问题…终于!
我们能不能引入一个代理,这个代理可以同时观察许多I/O流事件呢?
当没有I/O事件的时候,这个进程处于阻塞状态;当有I/O事件的时候,这个代理就去通知进程醒来?
于是,早期的程序猿们发明了两个代理—select
、poll
。
select、poll代理的原理是这样的:
当连接有I/O流事件产生的时候,就会去唤醒进程去处理。
但是进程并不知道是哪个连接产生的I/O流事件,于是进程就挨个去问:“请问是你有事要处理吗?”……问了99999遍,哦,原来是第100000个进程有事要处理。那么,前面这99999次就白问了,白白浪费宝贵的CPU时间片了!痛哉,惜哉…
- select是第一个实现 (1983 左右在BSD里面实现)
- 1997年实现了poll.
- select与poll原理是一样的,只不过select只能观察1024个连接,poll可以观察无限个连接。
上面看了,select、poll因为不知道哪个连接有I/O流事件要处理,性能也挺不好的。
那么,如果发明一个代理,每次能够知道哪个连接有了I/O流事件,不就可以避免无意义的空转了吗?
于是,超级无敌、闪闪发光的epoll,于5年以后, 在2002年被大神 Davide Libenzi 发明出来了。
epoll IO多路复用
epoll代理的原理是这样的:
当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个进程。如此,多高效!
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
epoll 现在是线程安全的。
epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
可是epoll 有个致命的缺点,只有linux支持。于是其他的平台实现类型的多路复用,比如BSD上面对应的是kqueue
, win下对应的iocp
。
epoll和select/poll区别
简单说epoll和select/poll最大区别是
- epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝
- epoll基于事件驱动,epoll_ctl注册事件并注册callback回调函数,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。
3. Nginx 异步,非阻塞,IO多路复用
Nginx 这样出众,正是他采用了异步,非阻塞,IO多路复用。
Nginx之前是单进程的。看下他的进程。1个master进程,2个work进程。
$ pstree |grep nginx
|-+= 81666 root nginx: master process nginx
| |--- 82500 nobody nginx: worker process
| --- 82501 nobody nginx: worker process
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
每进来一个request,会有一个worker进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request,并等待请求返回。那么,这个处理的worker不会这么傻等着,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”。于是他就休息去了。这就是异步
。此时,如果再有request 进来,他就可以很快再按这种方式处理。这就是非阻塞
和IO多路复用
。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。这就是异步回调
。
参考文件:
https://segmentfault.com/a/1190000007614502
https://www.zhihu.com/question/32163005
https://www.zhihu.com/question/22062795