浅谈网络编程 同步/异步/阻塞/非阻塞

这篇文章大部分内容,图片都来自《Unix网络编程》和《深入浅出Nodejs》这本两本书。如果有书的同学,可以直接去看书了,在我看来,目前还没遇到过比书上讲得更清楚的。如果没有,没关系,看我的文章也一样(反正我也是抄的,哈哈)。

image.png

阻塞与非阻塞

从简单的开始,我们以经典的读取文件的模型举例。(对操作系统而言,所有的输入输出设备都被抽象成文件。)
在发起读取文件的请求时,应用层会调用系统内核的I/O接口。
如果应用层调用的是阻塞型I/O,那么在调用之后,应用层即刻被挂起,一直出于等待数据返回的状态,直到系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的其他操作。
如果应用层调用的是非阻塞I/O,那么调用后,系统内核会立即返回(虽然还没有文件内容的数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超出了阻塞和非阻塞的辨别范畴。)
这便是(脱离同步和异步来说之后)阻塞和非阻塞的区别。总结来说,是否是阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即「抽离」去完成其他「任务」的则是非阻塞型的。
但任何技术都不是完美的,非阻塞I/O带来的是需要轮询技术去确定是否完成数据的获取,他让Cpu长期处理状态的判断也是一种CPU资源的浪费。
现在的轮询技术大概有以下几类

  1. read
    他是最原始的,性能最低的,在数据返回前,CPU一直用在等待上
  2. select
    他是在read上的一种改进,通过对文件描述符上的事件状态进行判断,但他有一个较弱的限制,他最多能同时检查1024个文件描述符
  3. poll
    在select上进行改进,采用链表的方式避免了数组长度的限制,但文件描述符较多时,他的效率还是十分低下的
  4. epoll
    是Linux下效率最高的I/O事件通知机制,在进入轮询时如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。他真实利用了事件通知。执行回调的方式,而不是遍历,极大地提升了效率

尽管epoll已经利用了事件通知降低了CPU的调用,但是,休眠期间的CPU几乎是闲置的,那么是否有一种理想的异步模型呢?

同步和异步

阻塞和非阻塞解决了应用层等待数据返回时的状态问题,那系统内核获取到的数据到底如何返回给应用层呢?这里不同类型的操作便体现的是同步和异步的区别。
对于同步型的调用,应用层需要自己去向系统内核问询,如果数据还未读取完毕,那此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(所以同步和异步并不决定其等待数据返回时的状态);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层即可以用取得的数据做其他相关的事情。
而对于异步型的调用,应用层无需主动向系统内核问询,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层即可以接收系统内核返回过来的数据,再做其他事情。
这便是(脱离阻塞和非阻塞来说之后)同步和异步的区别。也就是说,是否是同步还是异步,关注的是任务完成时消息通知的方式。由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。

总的来说,同步和异步关注的是任务完成消息通知的机制,而阻塞和非阻塞关注的是等待任务完成时请求者的状态。

Node的异步I/O

事件循环

在进程启动时Node便会创建一个while()循环,每次循环过程就查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果不再有,就跳出流程。


image.png

观察者

请求对象

对于Node中的􏰃􏰄I/O􏱺调用而􏰲言,回调函􏱹􏱺􏱻数􏳁不由开发者􏰮􏱺用。􏰺􏲵从我们发出调􏰫􏱺用后, 到􏱹􏱺􏱻回调函数被执行􏳗􏱝􏰥,中􏴹发生了什么?􏲴􏲵􏻣􏼓事实上,从JavaScript发起􏱺调用到内核执行完􏶔􏱝􏰥􏴃I/O􏳍操作的􏼔过程中,􏳌存在一􏰕中间产物􏻴,它􏷴叫做请求对象􏼕􏴕􏷓􏲬。
从js调用Node的核心模块,核心模块调用Cpp内建模块,内建模块再通过libuv进行系统调用。实质上调用的是一个叫uv_fs_open的方法,同时创建了请求对象(FSReqWrap)。包装完请求对象后,就将这个包装好的请求对象推入线程池等待执行,当线程池中有可用线程时,就会根据传入参数的类型调用相应的底层函数。至此,由JavaScript发起的异步调用第一阶段就此结束,JavaScript线程就可以继续执行当前任务后续操作。当前I/O操作在线程池中等待执行,不管是否阻塞I/O都不会影响JS线程的后续操作,达到了异步的效果

执行回调

请求对象送入线程池等待执行之后,异步I/O的第一步就完成了,回调通知是第二部分。I/O调用完成后,会将结果储存在req-result属性上,再通知IOCP,告知当前对象操作完成,并将线程还给线程池,在这个过程中事件循环中的观察者一折被调用,用于检查线程池中是否有执行完的请求,如果存在,就加入到I/O观察者队列中,再将其当做事件处理

屏幕快照 2017-06-21 下午10.52.37.png

小节

从前􏰡实􏰓􏰃􏰄I/O的过程􏰌􏰍中,我们可以􏱨􏵇􏰫􏰃􏰄提取出几个关于异步I/O的关键词:􏻬􏲿􏲱􏲹单线程、事件循􏰑􏰒 环、观察􏳡􏻜者,􏱖I/O线程池􏸱。这里就要问了单􏲣􏲹线程与I/O线程池􏸱之间好像冲突了􏴹􏶭。由于 JavaScript是􏲹单线程的,􏱪所以我们认为他不能驾驭多核CPU。事实上,在Node中, 􏱦除了JavaScript(主线程)是􏲹单线程外,Node自􏺎􏰳身是多线程的,只是I/O线程使用的CPU很少􏱇􏱈。

Java的I/O模型

《unix网络编程》里总结了五类IO模型。为了更好地理解,我会举一个叫外卖的例子来说明。

1. 完全阻塞模型

它的示意图如下:


就是说,如果我客户端发起了connect请求,那么当前线程就会休眠,等待服务端响应完毕,返回消息,才会继续走下去。这种代码,我们已经写过了:

socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("192.168.0.13", 8000));
System.out.println("Hello");

你会看到,在connect没有完成之前,最后第三行的println根本不会执行。也就是说客户端会阻塞在这个地方。
就好比,我叫个外卖,然后我就去大门口傻等着,外卖不送到,我也什么都不做,就坐在门口打盹,直到外卖小哥过来,把我叫醒,我才拿着外卖回家去吃。这样做显然效率不高,显得脑子有水。

2. 非阻塞式IO。

把非阻塞的文件描述符称为非阻塞I/O。可以通过设置SOCK_NONBLOCK标记创建非阻塞的socket fd,或者使用fcntl将fd设置为非阻塞。
对非阻塞fd调用系统接口时,不需要等待事件发生而立即返回,事件没有发生,接口返回-1,此时需要通过errno的值来区分是否出错,有过网络编程的经验的应该都了解这点。不同的接口,立即返回时的errno值不尽相同,如,recv、send、accept errno通常被设置为EAGIN 或者EWOULDBLOCK,connect 则为EINPRO- GRESS 。


就是说,客户端程序会不停地去尝试读取数据,但是不会阻塞在那个读方法里,如果读的时候,没有读到内容,也会立即返回。这就允许我们在客户端里,读到不数据的时候可以搞点其他的事情了。
仍然以外卖举例,就相当于,我一边扫地,一边等外卖。我不再像原来一样,在门口傻等了,而是扫两下,就跑到门口看看外卖到了没有。一直这样循环,直到我取到外卖,才从这个循环中跳出来,进入吃的流程。

3. IO多路复用

非常非常非常重要。可能是我的课程开始以来,最重要的一部分。请务必保证能理解透彻,这是NIO的核心与关键,也是高性能服务器的第一要诀。
最常用的I/O事件通知机制就是I/O复用(I/O multiplexing)。Linux 环境中使用select/poll/epoll_wait 实现I/O复用,I/O复用接口本身是阻塞的,在应用程序中通过I/O复用接口向内核注册fd所关注的事件,当关注事件触发时,通过I/O复用接口的返回值通知到应用程序。I/O复用接口可以同时监听多个I/O事件以提高事件处理效率。

这个代码我明天给出示例,今天先理解这张图的意思。还是外卖的例子,如果我们整栋楼的人,很多人叫了外卖,都有下楼来看外卖到没到的需求,于是物业就出了个招,让门卫小哥帮大家看着,整栋楼上的,不管是谁的外卖到了,先放到门卫小哥那里,然后门卫小哥再通知你下来拿自己的外卖。这样一来,我们就把本来多个人要跑去看自己的外卖到了这件事交给门卫小哥去做了。而我们解放出来,就可以继续看电视,打扫卫生,刷知乎了。由于我们可以继续 做自己的事情,外卖小哥和门卫小哥在同时也在工作,互不干扰,所以这种工作方式就被称为异步模型

4. SIGIO

除了I/O复用方式通知I/O事件,还可以通过SIGIO信号来通知I/O事件,如图所示。两者不同的是,在等待数据达到期间,I/O复用是会阻塞应用程序,而SIGIO方式是不会阻塞应用程序的。


上面这张图,就是我们现实生活中真正的外卖。数据到达以后,给客户端发一个消息,让客户端过来取数据。这就像外卖小哥到你家门口给你打电话,让你出来取一下。显然,这种是最方便的,也是最合理的。
但实际上,在真正的编程中,我们很少使用这种模型。

5. async io

POSIX规范定义了一组异步操作I/O的接口,不用关心fd 是阻塞还是非阻塞,异步I/O是由内核接管应用层对fd的I/O操作。异步I/O向应用层通知I/O操作完成的事件,这与前面介绍的I/O 复用模型、SIGIO模型通知事件就绪的方式明显不同。以aio_read 实现异步读取IO数据为例,如图所示,在等待I/O操作完成期间,不会阻塞应用程序。


这个图,如果对应到外卖有点不合适了,比较像网购空调,我所要做的,只是下单,而快递小哥会把空调送过来,他也不会让你自己去取,他会让安装师傅直接帮你安装。这个过程中,你什么都不需要做。你所要做的仅仅是发起一个请求。这种IO模型就是纯正的异步IO。
这种纯异步IO的最典型例子就是上面我们提到node.js中的callback。
这5种IO并不是相互对立的,通过一定的技巧,是可以相互转化的。


参考资料:
Java NIO(3): IO模型
深入浅出Nodejs

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容