彻底搞懂同步异步与阻塞非阻塞

时间:2023-02-15 09:04:02

上两篇文章讲过了BIO与非阻塞IO以及IO多路复用,洋洋洒洒近3万字。

这篇文章我们来聊一个很简单,但是很多人往往分不清的一个问题,同步异步、阻塞非阻塞到底怎么区分?

开篇先问大家一个问题:IO多路复用是同步IO还是异步IO

先思考一下,再继续往下读。


巨著《Unix网络编程》将IO模型划分为5种,分别是

  • 阻塞IO
  • 非阻塞IO
  • IO复用
  • 信号驱动IO
  • 异步IO

个人认为这么分类并不是很好,因为从字面上理解阻塞IO和阻塞IO就已经是数学意义上的全集了,怎么又冒出了后边3种模型,会给初学者带来一些困扰。

接下来进入正文。

作者:蝉沐风,
公众号:蝉沐风的码场

1. 一个简单的IO流程

让我们先摒弃我们原本熟知的各种IO模型流程图,先看一个非常简单的IO流程,不涉及任何阻塞非阻塞、同步异步概念的图。

彻底搞懂同步异步与阻塞非阻塞

客户端发起系统调用之后,内核的操作可以被分成两步:

  • 等待数据

    此阶段网络数据进入网卡,然后网卡将数据放到指定的内存位置,此过程CPU无感知。然后经过网卡发起硬中断,再经过软中断,内核线程将数据发送到socket的内核缓冲区中。

  • 数据拷贝

    数据从socket的内核缓冲区拷贝到用户空间

2. 阻塞与非阻塞

阻塞与非阻塞在API上区别在于socket是否设置了SOCK_NONBLOCK这个参数,默认情况下是阻塞的,设置了该参数则为非阻塞。

彻底搞懂同步异步与阻塞非阻塞

2.1 阻塞

假设socket为阻塞模式,则IO调用如下图所示。

彻底搞懂同步异步与阻塞非阻塞

当处于运行状态的用户线程发起recv系统调用时,如果socket内核缓冲区内没有数据,则内核会将当前线程投入睡眠,让出CPU的占用。

直到网络数据到达网卡,网卡DMA数据到内存,再经过硬中断、软中断,由内核线程唤醒用户线程。

此时socket的数据已经准备就绪,用户线程由用户态进入到内核态,执行数据拷贝,将数据从内核空间拷贝到用户空间,系统调用结束。此阶段,开发者通常认为用户线程处于等待(称为阻塞也行)状态,因为在用户态的角度上,线程确实啥也没干(虽然在内核态干得累死累活)。

2.2 非阻塞

如果将socket设置为非阻塞模式,调用便换了一副光景。

彻底搞懂同步异步与阻塞非阻塞

用户线程发起系统调用,如果socket内核缓冲区中没有数据,则系统调用立即返回,不会挂起线程。而线程会继续轮询,直到socket内核缓冲区内有数据为止。

如果socket内核缓冲区内有数据,则用户线程进入内核态,将数据从内核空间拷贝到用户空间,这一步和2.1小节没有区别。

3. 同步与异步

同步异步主要看请求发起方对消息结果的获取方式,是主动获取还是被动通知。区别主要体现在数据拷贝阶段。

3.1 同步

同步我们其实已经见识过了,2.1节和2.2节中的数据拷贝阶段其实都是同步!

注:把同步的流程画在阻塞和非阻塞的第二阶段,并不是说阻塞和非阻塞的第二阶段只能搭配同步手段!

同步指的是数据到达socket内核缓冲区之后,由用户线程参与到数据拷贝过程中,直到数据从内核空间拷贝到用户空间。

因此,IO多路复用,对于应用程序而言,仍然只能算是一种同步,因为应用程序仍然花费时间等待IO结果,等待期间CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。

select为例,用户线程发起select调用,会切换到内核空间,如果没有数据准备就绪,则用户线程阻塞到有数据来为止,select调用结束。结束之后用户线程获取到的只是「内核中有N个socket已经就绪」的这么一个信息,还需要用户线程对着1024长度的描述符数组进行遍历,才能获取到socket中的数据,这就是同步。

举个生活中的例子,我们给物流客服打电话询问我们的包裹是否已到达,如果未到达,我们就先睡一会儿,等到了之后客服给我们打电话把我们喊起来,然后我们屁颠屁颠地去快递驿站拿快递。这就是同步阻塞。

如果我们不想睡,就一直打电话问,直到包裹到了为止,然后再屁颠屁颠地去快递驿站拿快递。这就是同步非阻塞。

问题就是,能不能直接让物流的人把快递直接送到我家,别让我自己去拿啊!这就是异步。

3.2 理想的异步

我们理想中的完美异步应该是用户进程发起非阻塞调用,内核直接返回结果之后,用户线程可以立即处理下一个任务,只需要IO完成之后通过信号或回调函数的方式将数据传递给用户线程。如下图所示。

彻底搞懂同步异步与阻塞非阻塞

因此,在理想的异步环境下,数据准备阶段和数据拷贝阶段都是由内核完成的,不会对用户线程进行阻塞,这种内核级别的改进自然需要操作系统底层的功能支持。

3.3 现实的异步

现实比理想要骨感一些。

Linux内核并没有太惹眼的异步IO机制,这难不倒各路大神,比如Node的作者采用多线程模拟了这种异步效果。

比如让某个主线程执行主要的非IO逻辑操作,另外再起多个专门用于IO操作的线程,让IO线程进行阻塞IO或者非阻塞IO加轮询的方式来完成数据获取,通过IO线程和主线程之间通信进行数据传递,以此来实现异步。

彻底搞懂同步异步与阻塞非阻塞

还有一种方案是Windows上的IOCP,它在某种程度上提供了理想的异步,其内部依然采用的是多线程的原理,不过是内核级别的多线程。

遗憾的是,用Windows做服务器的项目并不是特别多,期待Linux在异步的领域上取得更大的进步吧。

4. 异步阻塞?

说完了同步异步、阻塞非阻塞,一个很自然的操作就是对他们进行排列组合。

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

但是异步阻塞是什么鬼?按照上文的解释,该IO模型在第一阶段应该是用户线程阻塞,等待数据;第二阶段应该是内核线程(或专门的IO线程)处理IO操作,然后把数据通过事件或者回调的方式通知用户线程,既然如此,那么第一步的阻塞完全没有必要啊!非阻塞调用,然后继续处理其他任务岂不是更好。

因此,压根不存在异步阻塞这种模型哦

5. 千万分清主语是谁

最后给各位提个醒,和别人讨论阻塞非阻塞的时候千万要带上主语。

如果我问你,epoll是阻塞还是非阻塞?你怎么回答?

应该说,epoll_wait这个函数本身是阻塞的,但是epoll会将socket设置为非阻塞。因此单纯把epoll认为阻塞是太委屈它,认为其是非阻塞又抬举它。

具体关于epoll的说明可以参见IO多路复用中的epoll部分。


完~