2

彻底搞懂同步异步与阻塞非阻塞 - 蝉沐风

 1 year ago
source link: https://www.cnblogs.com/chanmufeng/p/17121427.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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


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

  • 非阻塞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部分。



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK