之前这个零零碎碎的整理了一些,这里细致整理下IO多路复用的相关内容。
这里要明确两点,是什么,以及怎么做。
这个主要参考之前的一篇
之前这一篇列出了一些相关的内容,再这里再写下:
首先明确的是这两点:
同步与异步区别,关注的主要关注的是消息通信机制。
阻塞与非阻塞的区别,关注的主要是程序(调用者本身)在等待调用结果(消息 返回值)时的状态。
同步与异步区别,主要关注的是消息通信机制
所谓 同步调用 ,就是由调用者主动等待这个调用的结果。发出一个调用,在没有得到结果之前,该调用就不返回。一旦调用返回,就得到返回值了。
所谓 异步调用 ,调用在发出之后,这个调用结果就直接返回了。当一个异步调用过程在发出之后,调用者不会立即得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过函数回调来处理这个调用。
用于理解的场景:
比如去银行办理业务,一直排队等待,等待到才办理,这种就是同步的方式,即由处理消息者自己去等待消息是否被触发。
另外一种,就是先排队领取一个号码,这样可以去做其他事情了,等到号码到了的时候,再去办理交易,这中间可以做其他的事情,这样处理消息的一方就不用一直等待了,会由某种触发机制来通知处理消息者。
比如ajax的那种回调方式,就是采用的第二种的模式。
关注的是: 程序(调用者本身)在等待调用结果(消息 返回值)时的状态
阻塞调用是指调用结果返回之前,当前线程会被 挂起 。调用线程只有在得到结果之后才会返回。
非阻塞调用是指,在不能立刻得到结果之前,该调用不会阻塞当前线程,当前线程还会继续执行下去。
阻塞调用是指线程在等待结果返回之前,不能做其他的事,非阻塞调用则是说,等待调用结果返回的时候,还可以同时做其他的事情。
这个问题实际上涉及到的是网络IO的几种模型
首先是说这个东西到底是干嘛的,首先明确IO分为网络IO以及磁盘IO,这里说的是 网络IO 。在服务端和客户端进行通信的情况下,双方实际上使用的是socket通信,那么对于服务端,需要从socket的一端read数据,比如有多个客户端和服务端通信的时候,如果没有任何额外的措施,其中一个客户端发送数据时候进行了延迟,那么服务端的进程就会一直阻塞在read调用的地方。这种当然是效率很低的方式。
对于Linux常用的IO操作,比如read和write,通常IO操作都是阻塞I/O的,也就是说当你调用read时,如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据收到。
具体这种情况应该怎么处理呢?
这样会造成设么问题呢?比如当服务器有1000个请求并发的时候,那需要分配1000个线程同时进行处理,对于一般的机器,比如4核机器,就会造成频繁的线程切换,在理想情况下,每个核要在250个线程之间来回切换。
从资源开销的角度上来讲,如果每个线程需要512k来存放自己的栈,(对于goroutine这种协程来说,只要2k的额外栈空间)1000个线程就需要512M内存,4G内存最多支持8000并发。
说道这里其实会发现,好多时候的等待都是不必要的,聪明的人们就想到,可不可能有这种机制呢,比如好多socketpair,os可否将这些链接都监听起来,其中哪一个socket准备好了之后,就发一个event信息给这个负责监听的线程,负责监听的线程get到这个消息,就生成一个新线程去专门处理这个可以读出信息的socket端,这样就实现了精准定位,需要的时候才生成线程处理,避免了资源浪费。
这个就是所谓的IO多路复用(IO Multiplexing)的原理,这里的复用在内核层面的体现就是 单个线程通过记录跟踪每一个Sock(I/O流)的状态,来同时管理多个I/O流,通过这种机制可以提高服务端的吞吐能力 。说白了就是,通过一个线程将所有请求的状态监控起来,之后哪个请求准备好了,即是说服务端可以从这个请求中接收数据了,就生成新的线程,开始为这个请求提供服务了。
常见到的 select poll epoll 实际上就是os层对于IO多路复用的体现,当然它们出现的状态是有先后的,在仔细想想,实际上就是通过 事件机制+状态机 的这种方式来实现效率的提升。其实类似的这种思路在好多方面都在使用,比如分布式中的raft算法,进程的不同状态切换,内存资源的回收,状态机都是一个很重要的模型,之后通过事件机制控制识别状态之间的转换,不同的事件对应不同的action。
这一部分就是介绍下select poll 以及 epoll的具体功能。这部分是具体摘抄 知乎 上的:
I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。 select 被实现以后,很快就暴露出了很多问题。
select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。(这里可以看到,还是要轮询的)
select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如 poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。
但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。
于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如: epoll 现在是线程安全的。 epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。
其实有些国内知名厂商把epoll从安卓里面裁掉这种脑残的事情我会主动告诉你嘛。什么,你说没人用安卓做服务器,尼玛你是看不起p2p软件了啦。
而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。
上文的主要内容来源: https://www.zhihu.com/question/32163005 https://www.zhihu.com/question/28594409 https://www.zhihu.com/question/20114168