Pulpcode

捕获,搅碎,拼接,吞咽

0%

阻塞,非阻塞,同步,异步到底是个啥?

我发现周围一部分程序员,把阻塞,非阻塞,同步,异步这四个概念混淆了,一些没有理解这几个概念之间的关系,另一些则以为自己理解了。
其实很长一段时间我也没有搞明白这四个之间的关系与区别,之后有了一些工作经验,并在阅读了一部分书籍之后才豁然开朗,所以这篇博客打算把自己的经验分享出来。

常见的误区

一种最常见的误区是把”阻塞,非阻塞”作为一组,“同步异步”作为一组,然后做一个组合,然后分别讨论:

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

这样一下子就变成了四个模型,网上大多数劣质的博客,都是这样介绍的。

很神奇的是,如果都异步了还阻塞个什么。。。

单纯谈概念

首先我们不谈论io,也不谈论异常机制,只谈论这几个词的单纯概念。

阻塞非阻塞

阻塞的英文为:blocking,非阻塞的英文为:non-blocking,其实这两个词是针对等待一个任务时,当前线程的状态。
比如你在当前线程中执行一个任务,如果你必须等到此任务返回结果后才能继续执行,那你的这个任务就是阻塞的。
而如果你在执行一个任务后,并不需要马上得到结果,所以你依旧获得你当前线程的控制权,这里你的任务就是非阻塞的。
而你最后到底是如何获得结果,其实阻塞非阻塞并不能描述,它只能描述你当前线程在执行一个任务后的状态。所以阻塞也就常常预示着让出CPU。

同步异步

同步的英文是 Synchroscope, 异步的英文是 Asynchronous。这两个词是针对你是否主动等待一个任务的结果,它更偏向于消息通信。
比如你必须等到一个任务的结果返回,才能继续干别的事情,那这个任务就被成为同步的,再比如你要读取别人的接口才能进行计算。
而你并不需要等待一个任务的结果返回(将来这个任务执行完,可能会通知回调你),那这个任务就被称为异步的。比如你需要将当前的数据写一份到缓存,但是你此时的逻辑,并不关心写的结果是什么,那么这个写任务就可以异步执行。
所以它更偏于任务与任务之间的通信关系,执行的串并行。

这一点就说明,同步异步,和阻塞非阻塞,就是在描述不同的问题,虽然看上去同步就代表阻塞,异步就代表非阻塞,而这点也是造成搞混的原因之一。

聊聊IO

这几个概念被搞混,很大一部分是因为IO。

先说结论,在unix网络编程中,一共提到五种io模型,前四种都是同步的,也就是说阻塞的blocking io,和non-blocking,多路复用,信号驱动都是同步的,只有最后一种才被称为异步io,这五种io我会一一介绍。
所以说,同步阻塞,同步非阻塞的叫法,很可能是从这里衍生出来的。然后也就顺势“编出”了异步阻塞,异步非阻塞的奇怪概念。

阻塞IO

blocking-io

上图是linux网络编程提到的第一种IO,也就是阻塞IO,这个模型提到的用户缓冲区和内核缓冲区,我在之前的博客中提到过。可以从图中看到,用户的进程一直阻塞到数据被从内核缓冲区拷贝到用户进程缓冲区。
这是一种很常见的IO模型,普通的socket读取数据就是这种方式。

1
2
3
4
5
6
7
if ((nread = read(sock_fd, buffer, len)) < 0)
{
if (errno == EWOULDBLOCK)
{
return 0; //表示没有读到数据
}else return -1; //表示读取失败
}else return nread;读到数据长度

非阻塞IO

nonblocking-io

可以看到非阻塞IO模型中,在进行系统调用之后,并不会阻塞,而是立即返回,不过需要我们不停的轮训看数据是否处于就绪状态。

1
2
3
4
5
6
7
8
9
  flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, flags|O_NONBLOCK);

while (send(sockfd, snd_buf, sizeof(snd_buf), MSG_DONTWAIT) == -1)
{
sleep(1);
printf("sleep\n");
}
printf("send:%s\n", snd_buf);

多路复用(select, poll, epoll)

multiple-io

这个模型其实使用的也是非阻塞IO,只不过同时轮训多个非阻塞IO的状态,有效的利用轮训资源。所以可以看出这很适合处理大量的请求,比如web服务。
还需要注意的是,当用户进程调用了select,那么整个进程会被block,直到任意的一个socket准备好。
很多人以为这个模型是异步的,实际上它也是同步的。认为异步是因为把系统层和应用层搞混,比如我们常常说python的web框架,tornado是异步服务器,所以理所当然的认为,它底层所使用的epoll也是异步的。其实说tornado是异步服务器,这只是针对应用级的,但是你说到epoll,就要提系统级了,而他并不是异步IO。就像很多语言说支持协程,只不过是封装出来的,而底层可能并不是原生支持。也许就是用线程包装的。那么为什么这么在乎原生呢?比如静态语言原生支持类型检查,而动态语言,用再多的工具封装提供类型检查的支持,也比不过静态语言。

信号驱动IO

sigio-io

我记得我写过unix下的信号驱动的UDP服务器demo。你要注册一个sigio,然后在死循环里等待信号通知,然后调用你的回调处理函数。
不过在真实的生产环境,应该不会用这种方式来搭建web服务器,估计有UPD服务器才玩,毕竟信号这东西会丢,而且大量的信号你还要维护一个队列,这就和轮训没啥区别了。

以上的四种IO模型都是同步的,只有最后一种才能称为异步IO,从下图可以看出,只有最后一种,当系统通知用户进程时,数据已经完全在用户的进程空间了,而其它四种,都需要用户进程自己从内核空间拷贝到进程空间。这才是被称为异步io的原因:在调用AIO之后,不需要再做任何和读取数据相关的等待或者copy了,唯一收到信号的时候,数据已经可用了。

异步IO

asynchronous-io

文件IO

以上都是指网络io,这里顺便提一下文件io,文件io不能设置非阻塞,都是阻塞的。这是因为文件io和网络io一开始就是不一样的,虽然在linux上一切都是文件,但是网络io要考虑延迟,而文件io并不需要。因为之所以能被称为非阻塞io,主要是因为你可以非阻塞的查看io是否处于就绪状态,也就是看内核缓冲区是否有数据,然而文件io始终是就绪的。当然并不是说异步的文件io没用,据我查阅到的资料,linux下是没有异步的文件io的,而windows下有,也就是:IOCP。
然而别说是文件的异步io了,在linux中,网络的异步io也没有,linux2.6加入的aio并不是纯异步,因为它不是基于内核提供的api,而是对同步的封装。

聊聊异常

我在读《深入理解操作系统》的时候,里面也提到了同步与异步,不过是关于异常的(是系统级别的异常,不是程序异常)。
就是在异常控制流这一章中,提到的异常类型,

async-exception
只有第一种被称为异步,其它都是同步,而之所以IO信号的异常才被称为异步,是因为这是一个随机发生的,由外部触发。而其它几个都与当前进程相关,要么是主动调用,要么是产生错误。
所以读到这里,我仿佛更好的理解到了,同步代表要建立交互,需要相关性,而异步表达一种无关性,那如果是这样,我是不是可以开个脑洞,认为TCP这种面相连接的是同步,而UDP这种算异步呢?