Jason Pan

透彻理解阻塞/非阻塞、同步/异步

黄杰 / 2022-02-10


阻塞/非阻塞、同步/异步这几个词汇常用在与 I/O 相关的接口、实现、框架、协议的描述。

在一些场景下,同步与阻塞表示几乎相同的含义;在另外一些场景下,同步和非阻塞也可以一起描述接口。

甚至针对同一描述对象,我们从不同角度去看待,都可能选用截然相反的词进行描述。在某个层次上表现为阻塞/非阻塞、同步/异步,也并不意味着其实现中一定使用了相同的手段。

本篇小文是我个人对这几个词汇的理解过程,错误之处望大佬们不吝指正。

一、字面含义

我们先分别看看 阻塞/非阻塞、同步/异步 的字面含义,能对这两组概念有最简单直观的理解。

1.1 阻塞(block)

直接通过 Google 图片来理解一下 “block” 的含义,就是前进的道路被阻挡了:

image-20220209112737634

这里要区分另外一个在线程同步时提到的概念 屏障(barrier)

1.2 同步(synchronous)

synchronous 这个词,英文单词拆解开由三部分组成,syn-chron-ous,就是在时间上是一起的,通常译作同步的、同时的

通常这个单词还表示速度、位置上一致,比如地球同步卫星,叫做 “geosynchronous satellite”。

同步是同时发生的,在时序上可预测;异步不是同时发生的,在时序上不可预测

更广泛点,如果 A 做了一件事的同时,B 去做另外一件事,如果 A 不做这件事,B就不去做另外一件事,这也是一种同步。

二、阻塞与同步关联与比较

2.1 联系

两组概念字面意思看似没有什么关系,为什么我们还会经常混淆这两组概念?

因为在计算机领域,经常会有这样的场景,将同步阻塞紧紧的关联在一起:

用户调用 API,它会挂起线程,直到它得到某种答案并将它返回给用户。


比如在 gRPC 的文档中,介绍 同步 RPC 调用非阻塞 API 时是分别有两段描述:

Synchronous RPC calls that block until a response arrives from the server are the closest approximation to the abstraction of a procedure call that RPC aspires to.

This tutorial shows you how to write a simple server and client in C++ using gRPC’s asynchronous/non-blocking APIs.

可见同步和阻塞常常一块使用,异步和非阻塞常常一起使用。

2.2 区别

阻塞和同步两个都是形容词,但它们描述的细节有不同偏重:

根据这两点,我们就能更好的理解,上边介绍的为什么 gRPC 中叫同步 RPC 调用(Synchronous RPC calls) 而不是称作非阻塞 RPC 调用:

2.3 组合

通过本节前面的介绍,可以理解 同步/异步阻塞/非阻塞 是两个方面的描述,因此这两方面概念可以组合出四种情况来描述接口或 I/O 模型,表中给出了每种组合下的案例:

阻塞 非阻塞
同步 read() / write()
gRPC 同步 API
read() / write() + O_NONBLOCK
异步 I/O 多路复用 (select()) aio_read()/ aio_write()
gRPC 异步 API

后边会继续对其中的案例进行分析说明。

三、案例分析

为了加深理解,我们通过一些常见的案例来理解一下阻塞、非阻塞、同步、异步以及它们的组合。

3.1 同步接口 read() / write()

为什么说 read() 接口是同步的?因为无论是打开文件时,是否增加了 O_NONBLOCK 标志位, read() 函数执行之后,我们能够明确的知道它是否执行成功。

这里说的同步,是指 read() 自身对于程序流的表现是同步的,当然 read() 更底层实现上对数据的处理可以是异步的。在 APUE 14.5 节中也有提到,“并不能把非异步 I/O 函数称作‘同步’的”。这里加引号的“同步”是指完成了真正的读写,比如 write 完成并落到磁盘持久化。

3.2 I/O 多路转接

I/O 多路转接(I/O multiplexing)的基本思想是将多个描述符设置为非阻塞的,然后将这些描述符加入感兴趣的描述符列表中,然后阻塞直到其中的某个描述符准备好进行 I/O 时返回。

select() 等函数本身是同步阻塞函数,为什么有些文章会将 I/O 多路转接复用作为异步阻塞的案例?下面谈谈我的理解。考虑 APUE 14.4 节上的这个图:

image-20220209172153310

telnet 命令执行的时候,实际是有两个 fd,一个是与用户端终端的接收输入和将服务端的返回输出给用户,另外一个与则是 telnet server 之间的用于将用户的输入传入,并接收其返回。

当这两个 fd 建立起来之后,对这两个 fd 使用 I/O 多路转接后,因为无法预测是用户先输入的还是远端的机器先有返回,即对不同的 fd 的处理时序是无法预测的,也就是异步的。

3.3 gRPC asynchronous/non-blocking APIs

上文提到,gRPC 描述 API 时使用了 asynchronous/non-blocking:

3.4 I/O 模型

详见 UNP 6.2 节。

POSIX 在对 Synchronous I/O Operation 的定义中,同步和阻塞几乎是等价的:

An I/O operation that causes the thread requesting the I/O to be blocked from further use of the processor until that I/O operation completes.

Note: A synchronous I/O operation does not imply synchronized I/O data integrity completion or synchronized I/O file integrity completion.

3.5 异步 I/O

详见 APUE 14.5 节。

3.6 事件驱动

与事件驱动(Event-Driven)类似的表述还有 Event-Based / Event-Engine。

非阻塞 I/O 本身并不能直接带来性能的提升,而是需要结合 select/epoll 等多路复用机制。libevent 等事件驱动框架,对不同平台上的这些机制进行封装,将 fd 上发生可读、可写、超时等抽象成事件,指定在 fd 上发生某事件时进行处理的回调函数,实现整体功能。

四、参考资料