同步和异步怎么理解?
同步和异步描述的是调用方式。即如何获取到数据的。
调用方发起调用后,等待被调用方处理完数据后再将结果给到调用方。期间始终只有一个连接。这是同步。
调用方发起调用后,被调用方可以立即返回,结束这一次连接。被调用方计算出结果后,主动和调用方建立连接,将计算结果给到调用方。期间至少有两次连接。这是异步。
无论是从底层的IO
角度,还是应用层的api调用等。均可以这样简单的理解。
从IO
角度来说,区分同步异步很简单,如果是自己找内核拿的数据(只要内核使用recv
函数读数据),那就是同步,如果是内核将数据直接送入调用方,就是异步。这里指的数据就是实实在在的数据,而不是状态。所以真异步应该是AIO
。只要内核自己recv
就不能保证每次被调用方数据都能准备好。
阻塞和非阻塞怎么理解?
描述的是调用时的状态,调用方调用后一直在等待被调用方返回消息,并且期间不能干别的事,只能等,就叫这个链接当前是阻塞的。如果调用方调用后,可以去干别的事,等到被调用方完成后,再以某种方式通知调用方,就叫非阻塞。
从底层的IO
角度,或是应用程的API
调用等,可以简单的这么理解。
从底层IO
角度来看,区分阻塞和非阻塞,在于调用方线程有没有被挂起。挂起则是阻塞,没有则为非阻塞。BIO
下recv
必然导致挂起,等待返回数据。NIO
下recv
函数在内核没有准备好数据时会立即返回。
以上总结
同步异步说的是调用方获取数据的方式。
阻塞非阻塞说的是调用方的线程有没有被挂起,是否需要等待。
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
将会把数据copy
到buf
中,由协议传给用户态的线程,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
。
上述select
及BIO
和NIO
中所说的socket
,为了简约逻辑,一笔带过了一些东西。即内核是怎么获得这些连接的?实际上是通过recvfrom
和select
函数中的参数传递过去的,由于linux
系统中一切皆文件,所以调用这些函数,会将连接(文件描述符)也传递给内核。
缺点:
- 能传递的文件描述符有限,表明最大连接数有限,1024 个。
- 内核实际还是以轮询的方式查看这些连接的数据有没有准备好。轮询意味着
O(N)
。 - 1024 个文件描述符通过
select
函数调用内核,则意味着有很多数据要从内核态,拷贝到内核态。poll
poll
在select
基础上,解决了第一个问题。例用链表结构装载文件描述符。select
使用的是数组。epoll
epoll
利用事件回调解决轮询问题。使用用户态和内核态共享一块内存解决拷贝问题。
AIO为何不同和现状
AIO
是以上模型中唯一的异步IO模型。它的特点是不需要自己拿数据,内核处理好数据后,送过来(内核自己处理将内核态数据复制到用户态中),而不只是通知。
在多路复用中,select/poll,epoll
,是阻塞的。在AIO
中,没有阻塞,是纯正的异步非阻塞IO
模型。并且只有一次系统调用,即在请求时。
目前只有windows
系统有AIO
。JAVA
中的AIO
也只能在windows
系统中完成,在Linux
中JAVA
的AIO
还是使用的epoll
的函数。Linux
中的AIO
接口还不成熟。