Intro

默认情况下Dart程序只有一个控制流(isolate),如果有什么耗时的操作被发起,那么整个程序会被阻塞。

异步操作允许你的程序仅仅是发起一个操作,无需阻塞地等待该操作的结果,而是可以接下去做别的事情。

Dart语言中使用future来表示一个异步操作。一个future在程序中的表现形式是一个Future<T>的对象(其中T代表这个异步操作最终的结果的类型,如果不返回结果,则Tvoid)。

当一个future被发起,它会:

要了解异步的本质,我们需要先了解Dart的Event Loop.

Event Loop

Dart程序的运行顺序如下图所示:

event loop

先运行main(),结束后,程序并不是立马退出,而是挨个处理消息队列中的事件。

Event queue vs Microtask queue

Dart程序中有两类队列:

因此,我们有以下更细化的程序运行的活动图:

both queues

NOTE: 我们虽然可以预测不同的消息执行的顺序,但是不能预测它们真正被执行的时间点。因此,在Dart中如果发起了一个延时N秒的异步操作,它并不能保证N秒以后一定被执行(例如,这个异步操作之前有另一个异步操作需要更长的时间)。

How to schedule a task

如果是发起一个event queue的任务,使用future的操作原语,包括:FutureAPI和await(下面会细说)。

如果是发起一个microtask queue的任务,使用scheduleMicrotask()。但是,由于90019002这两个bug,导致:

the first call to scheduleMicrotask() schedules a task on the event queue; this task creates the microtask queue and enqueues the function specified to scheduleMicrotask(). As long as the microtask queue has at least one entry, subsequent calls to scheduleMicrotask() correctly add to the microtask queue. Once the microtask queue is empty, it must be created again the next time scheduleMicrotask() is called.

另外,有以下几个要注意的点:

  1. The function that you pass into Future’s then() method executes immediately when the Future completes. (The function isn’t enqueued, it’s just called.)
  2. If a Future is already complete before then() is invoked on it, then a task is added to the microtask queue, and that task executes the function passed into then().
  3. The Future() and Future.delayed() constructors don’t complete immediately; they add an item to the event queue.
  4. The Future.value() constructor completes in a microtask, similar to #2.
  5. The Future.sync() constructor executes its function argument immediately and (unless that function returns a Future) completes in a microtask, similar to #2.

Async

Dart提供了两套方法让你写基于future的异步代码,分别是asyncawait

举一个例子:

有两套不相关的操作:

理论上它们是可以并发执行的,如果没有异步,我们只能线性地执行它们,代码如下:

String fetchFoo() {
    print("Fetching foo..."); // time consuming
    return "foo";
}
void processFoo(String foo) {
    print("Process $foo");
}

void doFoo() {
    processFoo(fetchFoo);
}

void doBar() {
    print("Doing bar...");
}

void main() {
    print("main starts");
    doFoo();
    doBar();
    print("main ends");
}

输出:

main starts
Fetching foo...
Process foo
Doing bar...
main ends

Future API

很显然,对于Foo的操作,我们可以通过异步的方式执行。改动如下:

String fetchFoo() {
    print("Fetching foo..."); // time consuming
    return "foo";
}
void processFoo(String foo) {
    print("Process $foo");
}

void doFoo() {
    Future<String>(() => fetchFoo()).then((v) => processFoo(v));
}

void doBar() {
    print("Doing bar...");
}

void main() {
    print("main starts");
    doFoo();
    doBar();
    print("main ends");
}

输出:

main starts
Doing bar...
main ends
Fetching foo...
Process foo

有几个需要注意的点:

  1. doFoo()的使用使用者(main())没有影响
  2. 只需要在原来同步调用的地方使用Future来异步调用即可。注意:表达式中的类型(Future<String>)是对应其构造函数中传入的函数(fetchFoo)的类型所对应的。

async/await

下面纯属个人理解,如果有问题请大家给我留言指出,不甚感激😀

Dart2中引入的await语法糖,可以让我们用写同步一样的方式来写异步代码。

首先,我们需要知道await的定义是什么:

In await expression, the value of expression is usually a Future; if it isn’t, then the value is automatically wrapped in a Future. This Future object indicates a promise to return an object. The value of await expression is that returned object. The await expression makes execution pause until that object is available. — language tour

(吐槽一下,这个定义为什么没有出现在专门讲async的Future那个page里啊🤢)

假设我们有以下的函数:

void Foo() async {
    await DoA();
    DoB();
}

(注意:调用await的函数必须在一个async函数中,这也是async唯一作用)

那么沿用上面的定义,其中包含了两种情况:

  1. DoA()本身返回一个future。那么,Foo()在执行到await DoA()的时候会执行DoA(),直到DoA()返回future。此时,DoA()事实上往event loop中加入了一个事件。

    DoA()返回future以后,Foo()会将await表达式后面的语句作为回调append(then())到这个future上去(例如这里的DoB())。

  2. DoA()不返回future。那么,接下来会先执行DoA(),然后await语法糖会将DoA()的返回值转变成一个future进行返回。

我试图在这里指出的有以下几点:

  1. async的作用仅为允许被修饰的函数内部调用awaitdetail),而一个函数到底是同步的还是异步的,完全取决于其内部实现。因此,在不改变API的情况下可以改变一个函数的同步异步属性
  2. 上面的例子中的await DoA()不是说将DoA()整体丢到event loop中等待被执行。而是直接运行DoA()直到它返回。从这个角度来看,有没有await都一样。但是,await的真正作用是它会影响后面的语句;

    • 所有后面的语句会被作为callback append到await所触发的那个future(事件)。就好比它们都作为那个future的then的callback
    • 假设有以下的语句:

        Future<String> foo()  {...}
        final a = await foo();
        print('${a.runtimeTpye}');
      

      那么,最终输出的是String。虽然foo()返回的是个Future<String>,但是,当aawait以后被使用的时候,它就类似then中的callback一样,已经被转换成最终的完成时的类型了(String)。

因此,我们上面的例子如果改动如下(错误的实现):

void doFoo() async {
    await processFoo(await fetchFoo());
}

输出:

main starts
Fetching foo...
Doing bar...
main ends
Process foo

这里在main函数结束前先输出Fetching foo...的原因是,这部分代码在await fetchFoo()的时候就已经被执行了,只有返回的foo才被用来构造future并发起异步。而后续的await processFoo()则是作为上一个future的回调被执行,执行的时候由于使用了await,所以会再一次创建一个future并发起异步,不过这些动作都是发生在main函数结束以后了,所以Process foo显示在main ends之后。

完全等价的改动应该是这样:

void doFoo() async {
    await null;
    await processFoo(fetchFoo());
}

(当然,你要在fetchFoo()前面加个await也没什么关系)

detail

Error Handling