Dart 之 异步
Intro
默认情况下Dart程序只有一个控制流(isolate),如果有什么耗时的操作被发起,那么整个程序会被阻塞。
异步操作允许你的程序仅仅是发起一个操作,无需阻塞地等待该操作的结果,而是可以接下去做别的事情。
Dart语言中使用future
来表示一个异步操作。一个future
在程序中的表现形式是一个Future<T>
的对象(其中T
代表这个异步操作最终的结果的类型,如果不返回结果,则T
为void
)。
当一个future
被发起,它会:
- 将对应的函数(和定义的回调函数)放入Event Loop,然后返回一个未完成的
Future<T>
对象 - 当这个
future
后续从消息队列被取出并处理完毕后,会返回一个结果或者错误
要了解异步的本质,我们需要先了解Dart的Event Loop.
Event Loop
Dart程序的运行顺序如下图所示:
先运行main()
,结束后,程序并不是立马退出,而是挨个处理消息队列中的事件。
Event queue vs Microtask queue
Dart程序中有两类队列:
- event queue: (低优先级)用于接收各种I/O,鼠标,绘画,定时任务,消息等系统事件,也用于接收程序中发起的事件(通过
future
) - microtask queue:(高优先级)用于接收那些希望在控制流返回给event queue之前完成的异步事件,当前仅可以接收程序中发起的事件
因此,我们有以下更细化的程序运行的活动图:
NOTE: 我们虽然可以预测不同的消息执行的顺序,但是不能预测它们真正被执行的时间点。因此,在Dart中如果发起了一个延时N秒的异步操作,它并不能保证N秒以后一定被执行(例如,这个异步操作之前有另一个异步操作需要更长的时间)。
How to schedule a task
如果是发起一个event queue的任务,使用future
的操作原语,包括:Future
API和await
(下面会细说)。
如果是发起一个microtask queue的任务,使用scheduleMicrotask()
。但是,由于9001和9002这两个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.
另外,有以下几个要注意的点:
- 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.) - 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(). - The
Future()
andFuture.delayed()
constructors don’t complete immediately; they add an item to the event queue. - The
Future.value()
constructor completes in a microtask, similar to #2. - 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
的异步代码,分别是async
和await
。
举一个例子:
有两套不相关的操作:
fetchFoo
,然后processFoo
doBar
理论上它们是可以并发执行的,如果没有异步,我们只能线性地执行它们,代码如下:
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
有几个需要注意的点:
doFoo()
的使用使用者(main()
)没有影响- 只需要在原来同步调用的地方使用
Future
来异步调用即可。注意:表达式中的类型(Future<String>
)是对应其构造函数中传入的函数(fetchFoo
)的类型所对应的。
async/await
下面纯属个人理解,如果有问题请大家给我留言指出,不甚感激😀
Dart2中引入的await
语法糖,可以让我们用写同步一样的方式来写异步代码。
首先,我们需要知道await
的定义是什么:
In
await expression
, the value ofexpression
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 ofawait 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
的唯一作用)
那么沿用上面的定义,其中包含了两种情况:
-
DoA()
本身返回一个future
。那么,Foo()
在执行到await DoA()
的时候会执行DoA()
,直到DoA()
返回future
。此时,DoA()
事实上往event loop中加入了一个事件。在
DoA()
返回future
以后,Foo()
会将await
表达式后面的语句作为回调append(then()
)到这个future
上去(例如这里的DoB()
)。 -
DoA()
不返回future
。那么,接下来会先执行DoA()
,然后await
语法糖会将DoA()
的返回值转变成一个future
进行返回。
我试图在这里指出的有以下几点:
async
的作用仅为允许被修饰的函数内部调用await
(detail),而一个函数到底是同步的还是异步的,完全取决于其内部实现。因此,在不改变API的情况下可以改变一个函数的同步异步属性-
上面的例子中的
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>
,但是,当a
在await
以后被使用的时候,它就类似then
中的callback一样,已经被转换成最终的完成时的类型了(String
)。
- 所有后面的语句会被作为callback append到
因此,我们上面的例子如果改动如下(错误的实现):
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
也没什么关系)
Comments