Dart中的异步

什么是异步?

在学习具体的内容之前, 我们先来弄清楚一个概念: 异步到底是什么?

事实上, 很多情况下, 我们都是以多线程的方式来接触异步的, 比如安卓的网络请求、文件IO、数据库的读写等. 在我们的第一印象里, 异步理所当然地就会与多线程挂钩.

那么, 异步和多线程真的是一回事吗?

我们来考虑一个实际问题: 做一顿饭需要很多个流程, 比如淘米、蒸米饭、洗菜、腌肉、切菜、炒菜等. 如果我们以同步的方式来完成这个任务, 那么流程应该是先淘米, 然后蒸米饭, 米饭蒸好后再依次进行洗菜、腌肉、切菜、炒菜这些步骤——显然, 这太费时间了: 如果我们以异步的方式来做这顿饭, 花费的时间就会少很多——比如在蒸米饭的时候我们可以把肉腌上, 顺便还可以洗菜和切菜. 显然, 这种异步方式并不是多线程——如果是多线程的话, 那应该是另外叫几个人一块来做菜.

如果我们把这个问题想通了, 我们就应该真正能明确异步任务的概念了: 异步任务只是让程序暂时停止当前请求的处理, 先去处理下一个请求, 当时机成熟后再回头去处理这个请求.

可能这个定义过于抽象, 我们可以再举一个例子: 我们对一个按钮注册一个点击事件, 显然这需要对这个按钮进行监听. 如果以同步的方式去监听, 那么程序会一直监听这个按钮直到被点击, 不会再执行后面的操作了, 这显然和我们所认知的事实不一样, 那么程序对按钮的监听就应该是异步的了. 那么程序是对每一个按钮单独开一个线程进行监听吗? 这当然不可能. 我们都知道, 按钮的监听是通过设置回调函数来实现的. 从这个事实出发, 我们就可以得到一个结论: 异步和多线程并不是等价的, 多线程是实现异步的一种途径, 除此之外也有其它可以实现异步方法.

Dart的消息机制

如果看懂了第一部分的结论, 我们就不会被一个事实所困惑: Dart的实现是单线程的.

Dart程序默认只有一个工作线程, 它工作在一个被称为”isolate”的空间内. “默认”这个词说明Dart程序可以有多个工作线程, 但是和其它语言不同的是, 线程的资源并不是共享的, 这也就是它们工作的空间被称为”isolate”的原因. 当然, 线程之间仍然可以进行通信, 这并不是我们今天学习的重点, 就不再过多展开了.

既然Dart默认只有一个工作线程, 并且线程的资源并不共享, 可以认为开发者在一定程度上是鼓励我们只使用默认的单线程去完成任务的. 既然开发者鼓励我们这么去做, 那就说明Dart在单线程环境工作下也可以实现异步——我们已经明白异步的实现并不止多线程一种方式, 所以这个事实我们也应该能坦然接受.

那么Dart到底是怎么实现异步的呢?

首先我们要了解一下Dart的消息处理机制.

一个isolate拥有两个消息队列: event队列和microTask队列. 在同步的情况下, 这两个队列是没有用的, dart的工作线程会按照顺序依次执行main函数中的代码: 但是在异步的情况下, 这两个队列就是不可或缺的了. 在第一部分我们提到过, 异步的另外一种实现就是回调. 这两个队列就是用来存储异步事件的回调的.

消息队列这个名词大家应该很熟悉了, 即使这里不再详细说明, 我们也知道event队列和microTask队列是怎么工作的了, 这里只说一下它们两个的区别.

event队列是默认使用的队列, 一般来说, 一旦用到异步, event队列是处理异步回调的首选. microTask队列的优先级比event队列更高, 如果遇到了高优先级但是出现完的任务, 我们可以将它放入microTask队列. 程序在从event队列取出事件之前, 会先检查microTask队列有没有任务, 如果有的话就会优先完成microTask中的任务. 这样一来, 高优先级但是晚出现的任务就可以通过microTask来”插队“, 从而尽快得到实现.

Future

Future是Dart用来实现异步的一个非常重要的类. 我觉得可以这么理解Future——占位对象. 当一个异步函数要返回一个数值时, 它没有办法第一时间把结果返回回去, 但是Dart是顺序执行的, 想要执行后面的代码, 就必须要获得一个返回值. 这是不是很像我们从网络上加载一个图片? 当我们要在APP上展示一张网络图片时, 我们首先要给空间设置一个占位图, 当图片加载好后, 再通过控件的回调把占位图替换为真正要显示的图片. Future的工作原理和这是差不多的, 它将回调函数放入event队列中, 自己先来冒充结果, 骗Dart继续处理后面的事件. 当回调函数前面的事件都处理完后, Dart就会将回调函数取出, 得到真正的结果, 这是Future在让结果替换为自己, 这样就完成了一个异步流程.

Future有一系列控制将回调函数放入消息队列的函数, 包括延迟一段时间放入的Future.delay()、用来同步一系列Future的Future.wait()等. 我们看到, Dart是通过改变回调函数的入队时间和优先级来控制异步任务的执行顺序的. 这种方式比使用多线程要稍微复杂一些, 思路也大相径庭. 但是这个Dart为了实现效率的提高所作出的妥协, 我们也只能接受.

async和await

asnyc和await实际上是对Future体系的一个简化, 它实现了Future的自动封箱和拆箱(或者说自动将占位的Future替换为异步结果), 这样我们就不用在代码中显示地去写Future了.

事实上, async关键字并没有什么实质性影响, 它只是用来声明await的作用域.

关于await关键字, 我们可以把它的工作原理理解成Future.then(). Dart在遇到await后, 首先将它标识的语句的结果装入Future中加入event队列, 再将紧跟着await所标识的语句的语句注册在then中进行回调. 上面的操作完成后, 开始执行该语句.

上面的话可能很抽象, 我们看一个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
functionA(){
print('A');
}

functionBC() async{
await functionB();
functionC();
}

functionB() async{
print('B1');
await print('B2');
print('B3');
}

functionC() async{
print('C1');
print('C2');
print('C3');
}

functionD(){
print('D');
}

void main() async{
functionA();
functionBC();
functionD();
}

这段代码的输出结果是:

A
B1
B2
D
B3
C1
C2
C3

我们来解释一下这个输出:

  1. 程序先执行到functionA(), 直接输出A, 没有问题.

  2. 程序执行到functionBC()

  3. 遇到了await functionB(), 将functionB的结果封装为FutureA, 并将其加入到event队列中, 同时将functionC的调用注册在FutureA的then()中

  4. 执行functionB(), 输出B1

  5. 遇到了await print(‘B2’), 将print(‘B2’)的结果封装为FutureB, 并将其加入到event队列中,同时将print(‘B3’)注册在FutureB的then()中

  6. 执行print(‘B2’), 输出B2.

  7. 执行functonD(), 输出D. 至此, main函数中的指令全部执行完成

  8. 因为print(‘B2’)执行完成了, 所以用print(‘B2’)的返回值替换FutureB, 但是程序中没有展示. 同时执行FutureB的then()回调, 输出B3. 至此, functionB执行完成

  9. 因为functionB执行完成了, 所以用functionB的返回值替换FutureA, 但是程序中没有展示. 同时执行FutureA的then()回调, 调用functionC, 依次输出C1、C2、C3

  10. 程序结束

对照上面的解释, 相信可以很清楚地理解async和await的工作流程了.