同步、异步、阻塞、非阻塞

四个概念傻傻分不清楚,经过一番查阅资料,现将当前的理解记录下来(不一定是对的)

首先需要明确的是,当我们在讨论 IO 是同步异步还是阻塞非阻塞时我们到底在讨论什么?

先说一个公认的知识点,系统IO的分类有五种:

  1. 阻塞IO
  2. 非阻塞IO
  3. 多路复用
  4. 信号驱动
  5. 异步IO

它们之间的关系是这样的:

在解释这几个概念之前还有两个先决知识需要了解下:系统调用和进程的生命周期

系统调用

程序在运行时,CPU 是区分内核态用户态两种状态的,程序在用户态时可以操作自己的堆栈,但是无法操作硬件如网口,键盘等。如果程序 需要操作硬件怎么办呢?那就发起一个系统调用,系统调用会将程序从用户态切换到内核态,然后由内核操作硬件,并把结果返回给用户态的程序。

进程生命周期

进程的生命周期涉及五种状态的转换:

理解了这两个概念后就比较容易解释上述五种IO类型了:

首先系统调用的前几步大概是这样:

  1. 进程发起系统调用
  2. 程序切换到内核态,由内核去操作硬件
  3. 内核将从硬件获取的数据保存到内核空间

在第1步,进程发起系统调用之后,如果进程由运行态切换到阻塞态,则该系统调用是阻塞IO:

  1. 内核将内核空间数据拷贝到用户空间
  2. 进程被唤醒,开始处理该次系统调用的数据

阻塞IO因为发起系统调用时就被切换到阻塞态,其他的代码在等待系统调用直到被唤醒之前将无法执行。

在第1步,进程发起系统调用后,如果进程没有切换到阻塞态,则该系统调用是非阻塞IO:

  1. 进程轮询系统调用,查看数据是否准备好
  2. 若已经准备好,则进程切换到阻塞态,等待数据从内核拷贝到用户空间
  3. 进程被唤醒,开始处理该次系统调用的数据

非阻塞IO是指用户发起系统调用时,系统调用会立刻返回结果,不过该结果只是告诉你是否完成。 进程此时可以运行其他的代码,但是需要轮询去查询该次系统调用的结果,直到明确告诉进程系统调用已经结束。 此时进程就会切换到阻塞态,等待内核把数据拷贝到用户空间,等待再次被唤醒。

当进程有多个不同的系统调用时,轮询的效率很差,所以开始考虑使用额外的线程做这个轮询的事情,多个系统调用都通过这一个线程来 调用,这就是IO多路复用,算是非阻塞IO的改进版。

再后来,系统打算彻底消除轮询,使用信号量来通知进程系统调用结果,这便是信号驱动式IO:

  1. 内核通过信号量通知进程系统调用结束
  2. 进程再次发起系统调用,并切换到阻塞态
  3. 内核将数据拷贝到用户空间,并唤醒进程处理数据

我们发现就算是非阻塞IO(包括多路复用和信号驱动),它们只是在发起系统调用的时候没有被阻塞,但是在内核拷贝数据到用户空间的过程中还是处于阻塞状态的, 如果连拷贝数据的过程也不阻塞进程的话,那便是异步IO:

  1. 内核将数据拷贝到用户空间,并通知进程处理
  2. 进程在合适的时机处理用户空间数据

对于异步IO来说,整个系统调用的过程都不会处于阻塞状态。

既然有异步,那便有同步了,没错,阻塞/非阻塞/多路复用/信号驱动都算作同步IO。(其实可以这样理解,只要系统调用的生命周期中有需要进程处于阻塞态的时间段,就称为同步IO)

请注意,我们以上所讨论的同步/异步,阻塞/非阻塞是特指系统调用,其实如果抛开这个专有领域来看的话,同步和阻塞是同义词,异步和非阻塞也是同义词。我们知道,一般编程语言都会在系统调用之上封装自己的各种IO,比如读取文件,监听网络请求等。因为是在操作系统这个抽象之上做的更高一层的抽象,所以这种较高层次的IO抽象其实不用区分异步和非阻塞的区别,它们表示的是一种含义:那便是我在等待IO的过程中能否执行其他代码,可以的话,那么这个IO就是非阻塞的,也是异步的,不可以的话,那么这个IO就是同步的,也是阻塞的。

Node.js 宣称自己是异步非阻塞的,但是其实它在linux系统中底层是使用 epoll 作为系统调用基础,而 epoll 属于多路复用的类型,严格上来说都不能称之为异步 IO。但是如果我们从更上层的抽象来看,它所宣称的异步其实是指异步事件驱动(从运作机制,和编码方式上来看),非阻塞是指运行 js 的线程不会被阻塞,和我们上面所讨论的具体的系统调用类型倒没有太大关系。

还有一种看法是,在抛开系统调用不谈时,阻塞/非阻塞是指进程的状态,异步/非异步是指调用的IO方法会不会阻塞进程。他们其实是从两个视角描述同样的一件事。