前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么是异步IO

什么是异步IO

作者头像
用户2781897
发布2020-11-02 11:27:47
1.3K0
发布2020-11-02 11:27:47
举报
文章被收录于专栏:服务端思维服务端思维

什么是异步IO

从API上来说,是一组非阻塞的IO API,不过这是废话。

换个角度:程序不因为IO调用而被阻塞,就可以说程序是异步的。

要理解这个回答,首先要知道什么是“异步”。

编程或者架构模型有4种。

1. 阻塞

A调用B后,一直等着B返回结果。

这是最广泛使用,也是最简单的一种模型。普通的函数调用、传统的阻塞IO都是如此。

2. 轮询

A调用B后,A不断去B那里查询返回结果。

这在耗时任务中经常出现。比如一种资源的创建非常耗时,服务A通知服务B创建,B返回给A一个任务id或者资源id,A不断轮询B检查任务是否完成以及完成结果。这种也非常常见。在架构设计中,为了减少服务之间的循环依赖,常常不会让B再回去调用A。这样一来,在一个基于http的体系中,轮询是唯一解法。

3. 阻塞回调

A调用B后,A什么都不做,直到B通知A已完成

这种模式并不是经常出现,而且它实际上是异步回调的一个子集。在(资源非常少,无法承接多个任务 | 或者A通知B进行的是其他任务的前提)时,可能会选择这种模型。

4. 异步回调

A调用B后,该干啥干啥,B通知A已完成后,再继续处理该任务的后续任务。

这种模型是本文主要想说的。

异步回调

我们从逻辑上分析一下,A调用B时,需要告诉B哪些东西。

  1. B需要知道任务结束后通知谁,所以A需要告知B,完成后需要执行的过程f
  2. A要告诉B做什么事情,所以至少需要指定调用参数arg

所以,常见的异步回调框架,比如libuv(c)vertx(java)nodejs,函数签名大多是:功能(参数, 回调函数)

例如

代码语言:javascript
复制
db.find('select * from xx', [], resultHandler);
function resultHandler(err, rows) {
  // ...
}

这里的f就是resultHandler,arg就是'select * from xx', []

但是,从语法上,没有人能保证这个调用真的跑到数据库里取回结果。比如在单元测试时,find函数可能会返回一堆预定义的数据。这样的过程显然不是“异步”的。

那么,如下find函数实现做法,能算“异步”吗?

  1. 把接收到的参数和函数打包到一个对象里
  2. 传到某个FIFO队列里
  3. 一组线程池消费这个队列,然后执行Thread.sleep 10s,然后用预定义的数据调用传进来的函数(resultHandler)

emmmmm……

这样肯定也不是。因为这种做法本质上和直接sleep 10s没有任何区别。此处,把sleep 10s换成其他阻塞IO(比如write/read)是一模一样的。

但是这种做法已经比较接近了。

真正的异步

定时器

我们先不管IO。 如果你想实现一个异步的“sleep”函数,你会怎么做?

我们能从硬件拿到的只有当前时间,那么除了真的Thread.sleep还有别的方法吗?答案是没有。但是,等待的方式可以完全不一样。

假设,我们想在12:00开始等待10分钟。我们实际上告诉等待线程的,其实是“我们想最早在12:10收到回调”。等待线程可以选择每隔几毫秒检查一次当前时间,然后在时机合适时触发回调。

可能有人想说,这tm不还是总共sleep了10分钟吗,有什么区别?

这种情况下没有任何区别,但是如果你有两个定时任务呢?

还是假设我们现在是12:00。一个任务需要sleep 10分钟,另一个任务需要sleep 15分钟。按照之前的做法,需要占用两个线程。而现在只需要占用一个线程。如果任务数量继续往上增加,比如10000个任务,老做法需要占用10000个线程,而新做法依然只需要占用1个线程。

当然我们可以做的好一点,利用中断。比如第一个任务sleep10分钟,那么线程就直接sleep 10分钟。第二个任务在12:01分进来,只要sleep 5分钟。那么就可以中断线程,然后sleep5分钟。触发第二个任务回调后,再sleep4分钟,触发第一个任务回调。不过两者本质上是差不多的。

IO

从“定时器”的例子里看到,只有一个线程放在那死循环,就可以完成成百上千个任务。原理是“仅当任务完成时,触发对应的任务回调”。

放到通用的IO,这个道理也是一样的(其实定时器也是IO的一种)。

IO的正常事件只有两个:1.可读,2.可写。异常事件通常是连接异常、连接断开、资源问题等。

按定时器的原理,异步IO原理可以扩展为:“仅当事件触发时,才进行回调”。

这些在应用层是无法感知的。比如说,写缓冲没满,那么fd是可写的;读缓冲有数据,那么fd是可读的。但是应用层感知不到网卡队列。所以这些事情只能让内核来做了。

Linux Epoll

以往的select和poll,本质是轮询fd,看是fd是否可读或者可写等。原理就是遍历指定的几个fd,检查它们的可读写状态,然后告诉应用层。select要做两次数据拷贝,poll做一次。 select和poll其实也是异步,只不过需要不断轮询,而且复杂度都是O(n),比较慢。

Epoll的出现解决了几乎所有select和poll的缺点。当事件触发时,会直接告诉epoll事件已触发,在查询事件时(epoll_wait),只需要拷贝对应的链表而无需轮询。

但是,epoll不接收回调函数,它只是通知你fd事件激活。当事件激活后,你需要自己去调用对应的回调。所以,你仍然需要自己写一个死循环不停调用epoll_wait。

有人可能不理解,为什么死循环不停调用就是异步了?这个线程不还是被“阻塞”了吗?

有这种问题说明你对“阻塞”理解完全错误。阻塞并不是说“下面的代码还没有被执行”。CPU一条条指令执行下来,如果你写了一个死循环,而且有一个cpu核心就是钻牛角尖似的不停执行,那这就不是“阻塞”。阻塞是说,cpu目前已经不执行你这段代码了,但是下面的代码还没有被执行。比如你调用了阻塞版本的write,然后对端没有读取,那么线程就阻塞在那里,没有cpu会去执行后面的代码。

Epoll也会“阻塞”?

epoll_wait允许传一个超时时间。如果超过了这个时间还没有事件发生,会返回给你一个空list。也就是说,epoll也是可以“阻塞”线程的。但是我们不认为程序是“阻塞”的。因为,当epoll阻塞线程时,这时并没有任何事件需要处理。就像下面这两段程序:

代码语言:javascript
复制
Thread.sleep(1000);

以及

代码语言:javascript
复制
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start > 1000);

如果当作黑盒,两者效果是一模一样的。那么与其狂转cpu,还不如等着好了。我们之前的“定时器”示例也是如此,还不懂的话用那个例子思考一下,应该是能理解的。

— 本文结束 —

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-10-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 服务端思维 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是异步IO
  • 真正的异步
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档