阻塞非阻塞与同步异步区别

在网络编程中,经常会提到同步/异步,阻塞/非阻塞的概念,记得一开始的时候我总是分不清它们之间有什么区别,所以经常混淆。其实它们之间是有着一层包含与被包含的关系,其中同步包含了阻塞与非阻塞,而异步则是另一种情况。可以划分为三类:

  • 同步阻塞
  • 同步非阻塞
  • 异步

同步阻塞

Linux上的IO默认情况下均为阻塞IO(aio系列除外),所有的套接字也默认为阻塞套接字。下图以UDP数据报为例,展示了阻塞IO的基本过程:

调用recvfrom,其系统调用直到数据报到达并且被复制到用户缓冲区中或者发生错误才返回。我们所说的「阻塞」是指系统调用等待数据报达到这一行为。

这个过程就相当于一个人去书店买书,但是书卖完了,于是他就在书店一直等到书店到货并买到书之后才回家。

同步非阻塞

把一个套接字设置为非阻塞相当于在告诉内核:当一个IO操作所请求的数据并未到达或其他原因导致该操作不能进行时,不要把本线程投入睡眠,而是返回一个错误。

前三次调用recvfrom时数据报并没有到达,所以立即返回一个EWOULDBLOCK错误,这便是所谓的「非阻塞」,第四次调用recvfrom时数据包已到达,它就被复制到用户缓冲区后,recvfrom成功返回,程序接着往后执行。

同样以买书的例子来说,非阻塞这一过程就是一个人去书店买书,如果书卖完了就回家。后面可能伴随着另一行为就是过一段时间再来书店看看书到货没(这就是轮询)。

另外对于IO复用(epoll/poll/select)就相当于一个人要买书,他先打电话询问书店老板是否有货,如果有货就去书店买。如果没货,就让书店老板在到货的时候通知他。

异步

异步IO是由POSIX规范定义的。在Linux上主要是aio系列函数,它们的具体工作机制是:告知内核启动某个操作,并让内核在整个操作(包括把数据从内核复制到用户缓冲区这一过程)完成后再通知我们。

调用aio_read函数,传递给内核描述符、用户缓冲区指针、用户缓冲区大小和文件偏移,并告知内核当整个操作完成时如何通知我们(注册信号或回调)。该系统调用立刻返回且不阻塞用户线程,当内核完成整个操作并通知我们后,用户缓冲区就可以使用了(省去了用户把数据从内核拷贝到用户缓冲区这一过程)。

可以看到异步与同步的区别就在将数据从内核复制到用户缓冲区这一过程上。对于同步操作来说,这个过程是由用户来完成,用户在进行复制的过程中是不能做其他的动作的。而对于异步操作来说,将数据报复制到用户缓冲区这一过程是由内核来完成的,只是在内核完成这一操作后通知用户该缓冲区可以用了,用户在内核复制的过程中可以进行其他的动作行为。

相应的,对于买书这一行为就与同步时不同了。相当于一个人打电话向书店订购一本书,书店负责送货上门,书店有货就立即送货,如果没货就等到货后再送货,但是此人对这一过程不关心,他只知道当书店送货上门后就可以阅读这本书了。

总结

UNP中关于同步IO和异步IO有这样一段话:

POSIX把这两个属于定义如下:

  • 同步IO操作(synchronous I/O operation)导致请求阻塞,直到I/O操作完成。
  • 异步IO操作(asynchronous I/O operation)不导致请求阻塞。
    不管是阻塞IO还是非阻塞IO,当他们把数据从内核拷贝到用户缓冲区这一过程都是「阻塞」的(这里的阻塞与前文中同步阻塞/同步非阻塞中的「阻塞」意义不同,前文中的阻塞指系统调用等待数据准备好这一过程),所以阻塞IO/非阻塞IO都属于同步IO。需要注意的是异步IO并没有阻塞或者非阻塞一说。

在Linux上只有aio系列函数为异步IO,除此以外的其他IO函数均为同步IO(包括使用IO复用时候的各种IO)。