参考通俗讲解 异步,非阻塞和 IO 复用
一、异步,非阻塞
为了更加形象的说明同步异步、阻塞非阻塞,我们以小明去买奶茶为例。
1.1 同步与异步
同步与异步的重点在消息通知的方式上,也就是调用结果通知的方式。
- 同步: 当一个同步调用发出去后,调用者要一直等待调用结果的通知后,才能进行后续的执行。
- 异步:当一个异步调用发出去后,调用者不能立即得到调用结果的返回。
异步调用,要想获得结果,一般有两种方式:(1)主动轮询异步调用的结果;(2)被调用方通过callback来通知调用方调用结果。
生活中的例子
- 同步买奶茶:小明点单交钱,然后等着拿奶茶;
- 异步买奶茶:小明点单交钱,店员给小明一个小票,等小明奶茶做好了,再来取。
异步买奶茶: 小明要想知道奶茶是否做好了,有两种方式:
- 小明主动去问店员,一会就去问一下:“奶茶做好了吗?”...直到奶茶做好。这叫轮训。
- 等奶茶做好了,店员喊一声:“小明,奶茶好了!”,然后小明去取奶茶。这叫回调。
1.2 阻塞与非阻塞
阻塞与非阻塞的重点在于进/线程等待消息时候的行为,也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。
阻塞调用在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活.
非阻塞调用在发出去后,不会阻塞当前进/线程,而会立即返回。
生活中的例子
- 阻塞买奶茶:小明点单交钱,干等着拿奶茶,什么事都不做;
- 非阻塞买奶茶:小明点单交钱,等着拿奶茶,等的过程中,时不时刷刷微博、朋友圈。
1.3 总结
通过上面的分析,我们可以得知:
同步与异步,重点在于消息通知的方式;
阻塞与非阻塞,重点在于等消息时候的行为。
所以,就有了下面4种组合方式:
- 同步阻塞:小明在柜台干等着拿奶茶;
- 同步非阻塞:小明在柜台边刷微博边等着拿奶茶;
- 异步阻塞:小明拿着小票啥都不干,一直等着店员通知他拿奶茶;
- 异步非阻塞:小明拿着小票,刷着微博,等着店员通知他拿奶茶。
二、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流,
1.最初级的I/O复用
所谓的I/O复用,就是多个I/O可以复用一个进程。采用非阻塞的模式,当一个连接过来时,我们不阻塞住,这样一个进程可以同时处理多个连接了。
比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”
然后进程就一直从头到尾问这10000个连接,如果这1000个连接都没有I/O事件,就会造成CPU的空转,并且效率也很低,不好不好。
2.升级版的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 发明出来了。
3.epoll IO多路复用
epoll代理的原理是这样的:
当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个进程。如此,多高效!
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
- epoll 现在是线程安全的。
- epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
可是epoll 有个致命的缺点,只有linux支持。于是其他的平台实现类型的多路复用,比如BSD上面对应的是kqueue, win下对应的iocp。
4.epoll和select/poll区别
简单说epoll和select/poll最大区别是
- epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝
- epoll基于事件驱动,epoll_ctl注册事件并注册callback回调函数,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。
- 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
每进来一个request,会有一个worker进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request,并等待请求返回。那么,这个处理的worker不会这么傻等着,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”。于是他就休息去了。这就是异步。此时,如果再有request 进来,他就可以很快再按这种方式处理。这就是非阻塞和IO多路复用。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。这就是异步回调。