本文核心资料均来自网络。但通常孤立的网络资料总有零零碎碎之感,看完还是不通不透,浅尝辄止,觉得自己的体系拼图里总是缺少几块。
本人有幸在IO这个话题下找到几篇能够互相补充的资料,结合了这些资料,融进自己的理解和实现,以成此文。
如有不当,恳请斧正。

结论

IO

  • 服务器端关注同步异步,同步和异步指的是服务器如何处理消息通知的机制。
  • 客户端关注阻塞非阻塞,阻塞和非阻塞指的是程序(调用者/客户端)在等待调用结果(消息通知)时的状态。

厘清概念

经典解释1 《书店买书》

你打电话问书店老板有没有《分布式系统》这本书

  1. 同步与异步

    • 同步
      如果是同步通信机制,书店老板会说,你稍等,“我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
    • 异步
      书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
  2. 阻塞与非阻塞

    • 阻塞
      你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果。
    • 非阻塞
      如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了,当然你也要偶尔过几分钟check一下老板有没有返回结果。
      在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

经典解释2 《叫外卖》

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

  1. 完全阻塞模型
    如果客户端发起了connect请求,那么当前线程就会休眠,等待服务端响应完毕,返回消息,才会继续走下去。

    1
    2
    3
    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设置为非阻塞。

    就是说,客户端程序会不停地去尝试读取数据,但是不会阻塞在那个读方法里,如果读的时候,没有读到内容,也会立即返回。这就允许我们在客户端里,读到不数据的时候可以搞点其他的事情了。

    仍然以外卖举例,就相当于,我一边扫地,一边等外卖。我不再像原来一样,在门口傻等了,而是扫两下,就跑到门口看看外卖到了没有。一直这样循环,直到我取到外卖,才从这个循环中跳出来,进入吃的流程。

  3. IO多路复用模型
    还是外卖的例子,如果我们整栋楼的人,很多人叫了外卖,都有下楼来看外卖到没到的需求,于是物业就出了个招,让门卫小哥帮大家看着,整栋楼上的,不管是谁的外卖到了,先放到门卫小哥那里,然后门卫小哥再通知你下来拿自己的外卖。这样一来,我们就把本来多个人要跑去看自己的外卖到了这件事交给门卫小哥去做了。而我们解放出来,就可以继续看电视,打扫卫生,刷知乎了。由于我们可以继续 做自己的事情,外卖小哥和门卫小哥在同时也在工作,互不干扰,所以这种工作方式就被称为(伪)异步模型
    * (注) 原文这里写的是异步模型,与另一资料(也就是本文顶部的结论图)冲突,经过整合,我标注这里为伪异步。那么,问题来了,为什么这是伪异步,他和真异步的区别又是什么?下文解析。

    最常用的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. 纯异步模型
    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。
    延伸阅读 Nodejs全异步事件引擎libuv源码剖析之:高效线程池(threadpool)的实现
    这5种IO并不是相互对立的,通过一定的技巧,是可以相互转化的。

  6. 5个IO模型的区别

上述总结

  1. 服务器端关注同步异步,同步和异步指的是服务器如何处理消息通知的机制。
  2. 客户端关注阻塞非阻塞,阻塞和非阻塞指的是程序(调用者/客户端)在等待调用结果(消息通知)时的状态。

通俗解释

  1. 如果服务器没有通知(也就是说没有异步机制),客户端只能干等(阻塞/挂起)或者自己每过几分钟询问一下(非阻塞/轮询)。
  2. 如果服务器有通知(有异步机制),客户端直接不用傻等了(因此异步中通常不存在阻塞)。

撸袖实践

  1. 业界分别在什么典型产品上用到了上述模型?

    1. IO多路复用模型
      延伸阅读 Java 9、Spring Boot 2.0、HTTP/2
    2. 纯异步模型
      延伸阅读 Tomcat APR纯异步模式
  2. 分别实现一个伪异步和真异步程序

IO模型
关于同步、异步与阻塞、非阻塞的理解
怎样理解阻塞非阻塞与同步异步的区别?
Linux Network IO Model、Socket IO Model - select、poll、epoll
聊聊 Linux 中的五种 IO 模型