同步和异步怎么理解?

同步和异步描述的是调用方式。即如何获取到数据的。
调用方发起调用后,等待被调用方处理完数据后再将结果给到调用方。期间始终只有一个连接。这是同步。
调用方发起调用后,被调用方可以立即返回,结束这一次连接。被调用方计算出结果后,主动和调用方建立连接,将计算结果给到调用方。期间至少有两次连接。这是异步。

无论是从底层的IO角度,还是应用层的api调用等。均可以这样简单的理解。
IO角度来说,区分同步异步很简单,如果是自己找内核拿的数据(只要内核使用recv函数读数据),那就是同步,如果是内核将数据直接送入调用方,就是异步。这里指的数据就是实实在在的数据,而不是状态。所以真异步应该是AIO。只要内核自己recv就不能保证每次被调用方数据都能准备好。

阻塞和非阻塞怎么理解?

描述的是调用时的状态,调用方调用后一直在等待被调用方返回消息,并且期间不能干别的事,只能等,就叫这个链接当前是阻塞的。如果调用方调用后,可以去干别的事,等到被调用方完成后,再以某种方式通知调用方,就叫非阻塞。
从底层的IO角度,或是应用程的API调用等,可以简单的这么理解。
从底层IO角度来看,区分阻塞和非阻塞,在于调用方线程有没有被挂起。挂起则是阻塞,没有则为非阻塞。BIOrecv必然导致挂起,等待返回数据。NIOrecv函数在内核没有准备好数据时会立即返回。

以上总结

同步异步说的是调用方获取数据的方式。
阻塞非阻塞说的是调用方的线程有没有被挂起,是否需要等待。

IO 模型和多路复用的关系?

一共有五种IO模型,其中多路复用 IO是其中一种。

  • blocking IO,BIO,同步阻塞IO
  • nonblocking IO,NIO,同步非阻塞IO
  • IO multiplexing,多路复用IO或MIO(同步非阻塞)
  • asynchronous IO,AIO,异步非阻塞IO
  • signal driven IO,SIO,很少用到,以下不做经验总结

    IO 模型

    我将尽量在各个IO模型中,将我的理解和学习总结,写出来,希望各位老哥能参与进来讨论和斧正。

    BIO

    线程若要使用硬件资源,就要使用socket向内核打交道,当线程调用recvform时,内核这个时候还没有准备好数据,于是线程挂起阻塞,一直等待内核准备好数据,准备好数据后,recvform将会把数据copybuf中,由协议传给用户态的线程,recvform返回的是数据的字节数。即线程被阻塞了。
    这里的socket默认是阻塞的。

如果我们的服务,起了 10 个线程接收客户端的连接请求,如果是耗时操作,这 10 个连接会一直等待我们的服务将数据处理完毕,然后返回给客户端。这时如果再进来一个客户端,我们是服务不了的,要想再多服务几个客户端,就得多起几个线程。即每个客户端,都需要一个线程为其服务,以此满足并发情况。

优点:模式简单
缺点:无法同时满足大量连接请求,起多个线程将会消耗大量内存和CPU资源。体现在CPU单位时间内,CPU需要在每个时间片中分别执行各个线程的逻辑,线程越多,CPU需要切换执行的线程就越多,就会有更多的系统调用和切换成本。

BIO场景下,我们程序员无法解决,我们写的代码,起的线程,最终还是要跟内核打交道的。所以缺点无法被我们解决。只能从内核下手。然后就有了NIO

NIO

设置非阻塞的socket跟内核打交道。使用非阻塞的socket打交道后,同BIO不一样的是,即使内核还没有准备好数据,也不会把用户线程挂起,而是会立即返回一个信号给线程,告诉它没有准备好数据。(在这里可以看出,线程是同步请求的内核,但是线程并没有被阻塞,所以NIO虽然简称为非阻塞,但也可以叫做同步非阻塞。)
接着线程会不停的继续调用recvfrom,直到内核把数据给弄回来。

NIO解决了一个线程只能为一个客户端服务的情况,可以有效减少线程抛出数量,达到减少因线程过多引起内存占用,和线程过多引起上下文切换的损耗。

优点:减少线程抛出数,间接减少内存和因线程过多引起的过多线程之间的上下文切换。
缺点:但是带来了一个新的问题,即轮询也会调用recvfrom,这个函数会发生系统调用,而系统调用本身是中断行为,会导致用户态与内核态的切换。切换就需要保存上下文。并且大多数为无效循环,recvfrom很有可能多次读不到数据。

即,减少线程之间的切换,转换为了轮询。系统消耗都差不多,仅只是抛出的线程减少了。然后就有了多路复用。此时轮询动作起始在用户态空间。

多路复用IO

三种实现方式

select

引入了select函数。
内核提供了select函数,该函数的作用,是监视所有socket(线程),哪个socket的数据被内核准备好了,就会通知对应的用户线程。调用此select函数的线程是阻塞的。
被通知到的用户线程,接收到通知后,需要再次调用recevfrom函数去内核取数据。

优点:在这里,没有了用户态空间的轮询,该轮询动作由内核态完成,大大减少了内核态和用户态之间的切换。
同时没有了NIO中的无效循环。
缺点:完成这种模式,至少需要两次系统调用,一次是select,一次是recevfrom

上述selectBIONIO中所说的socket,为了简约逻辑,一笔带过了一些东西。即内核是怎么获得这些连接的?实际上是通过recvfromselect函数中的参数传递过去的,由于linux系统中一切皆文件,所以调用这些函数,会将连接(文件描述符)也传递给内核。

缺点:

  1. 能传递的文件描述符有限,表明最大连接数有限,1024 个。
  2. 内核实际还是以轮询的方式查看这些连接的数据有没有准备好。轮询意味着O(N)
  3. 1024 个文件描述符通过select函数调用内核,则意味着有很多数据要从内核态,拷贝到内核态。

    poll

    pollselect基础上,解决了第一个问题。例用链表结构装载文件描述符。select使用的是数组。

    epoll

    epoll利用事件回调解决轮询问题。使用用户态和内核态共享一块内存解决拷贝问题。
    IO 模型和多路复用总结 - 图1

    AIO为何不同和现状

    AIO是以上模型中唯一的异步IO模型。它的特点是不需要自己拿数据,内核处理好数据后,送过来(内核自己处理将内核态数据复制到用户态中),而不只是通知。
    在多路复用中,select/poll,epoll,是阻塞的。在AIO中,没有阻塞,是纯正的异步非阻塞IO模型。并且只有一次系统调用,即在请求时。

目前只有windows系统有AIOJAVA中的AIO也只能在windows系统中完成,在LinuxJAVAAIO还是使用的epoll的函数。Linux中的AIO接口还不成熟。