[译] Dart异步编程:Event Loop

209 阅读8分钟

原文地址:The Event Loop and Dart

基本概念

在写UI代码的时候,我们可能会熟悉事件循环与事件队列这样的概念。它们保证了图像操作和类似鼠标点击这样的操作能在某个时刻里只会执行其中的一个。

事件循环和队列

事件循环的工作就是从事件队列里取出一个事件然后执行它,只要队列里有条目,它就会一直重复的执行这两个步骤。

image

这些事件可能代表不同的东西,比如用户的输入、文件的I/O,计时等等。例如下面这幅图,就展示了包含计时器与用户输入事件的事件队列:
image

这个概念就算在非Dart语言下我们也可能熟悉,现在我们来看下Dart平台下是怎么使用它的。

Dart的单线程执行

一旦一个Dart方法开始执行,那么只要它还存在就会一直运行。换句话说,它不会被其它的Dart代码打断。

下图显示,当一个Dart app的main isolate执行它的main()方法时,就意味着该app开始运行了。main()方法执行后,main isolate的线程就会开始一个接一个的处理app的事件队列里的事件。

image

这是一个简化的描述。

Dart的事件循环和队列

一个Dart app 会拥有一个带着两个队列的单事件循环——事件队列(event queue)和微任务队列(microtask queue)。

事件队列包含所有的外部事件:I/O,鼠标事件,绘制事件,计时,Dart isolate 之间的消息通讯等。

微任务队列是很有必要的存在,因为当事件处理的代码有时需要晚一点完成一个任务,但又希望是在控制器交给事件队列之前。例如,当一个可观察对象(observable object)发生了改变,它将这些变化聚合在一起然后以异步的形式报告它们。微任务队列允许这个可观察对象在DOM显示不一致的状态之前报告这些变化。

事件队列包含的事件来自Dart和系统里的其它地方。目前微任务队列只包含来自Dart内部代码的条目(entries)

下面的图片展示当main()方法存在的时候,事件队列是如何工作的。

  • 首先它会以先进先出(FIFO)的形式执行任何一个微任务;
  • 然后它会出队和处理事件队列里的一个事件;
  • 一旦两个队列都是空的并且也不会再有事件,app的embedder(例如浏览器或测试框架)就会处理这个app。

一个点:如果web app 的用户关闭了窗口,那么web app可能会在事件队列为空之前退出。

image

注意:当事件循环正在执行一个从微任务队列里来的任务时,事件队列会被阻塞住,即app不能进行绘制图片,处理鼠标事件,响应I/O等。

尽管我们可以预测任务执行的顺序,但我们无法准确的预测事件循环会在什么时候从队列里取出任务。Dart事件处理系统是基于单线程循环的机制,它不是基于ticks或是任何时间测量机制。例如,当你创建一个延时任务,并且指定了该事件执行的时间。然而如果在该事件之前的所有事件没有被处理完,那么它也不会被执行的(在微任务队列里也是同样的道理)。

提示:通过链接futures来指定任务顺序

如果你的代码有前后依赖关系,那么就要明确地指出来。显式的依赖关系可以让其他开发者理解你的代码,也可以让他们更容易的重构你的代码。

举个例子:

///错误示范,因为没有显示指明设置和使用该变量之间的依赖关系

future.then(...设置一个重要变量...);
Timer.run(() {...使用一个重要变量...});
1234

因此应该这样写:

future.then(...设置一个重要变量...)
  .then((_) {...使用一个重要变量...});
12

更好的处理是使用then()来指明这个变量在使用前必须先设置(如果你想在发生错误后该代码依然执行,那么也可以使用whenComplete()来代替then())。

如果使用该变量需要花费一些时间,那么可以考虑把那部分代码放在一个新的Future里。

future.then(...设置一个重要变量...)
  .then((_) {new Future(() {...使用一个重要变量...})});
12

使用一个新的Future使得事件循环可以从事件队列里执行其它的事件。

怎样安排一个任务

当我们需要一些代码稍后执行,我们可以使用 dart:async 库所提供的API:

  • Future类,添加一个事件到事件队列的末尾
  • 顶级函数scheduleMicrotask() ,添加一个事件到微任务队列的末尾。

一个点:scheduleMicrotask() 曾命名为 runAsync()

合理地使用队列

尽量使用Future来安排任务进事件队列。使用事件队列可以保持微任务队列的短小,减少因微任务队列导致事件队列一直处于饥饿状态的可能性。

如果一个任务一定要在事件队列里的任何一个事件之前执行,那么通常你应该马上就执行该方法。如果不能,就使用scheduleMicrotask()添加一个item到微任务队列里。例如,在一个web app里使用微任务来避免过早地释放js-interop proxy或是结束IndexedDB事务。

image

有必要的话使用isolates或者workers

如果有一个计算密集型的任务需要执行应该怎么办?为了让app能够保持响应,应该把这个任务放到它自己的isolate或是worker里。Isolate会根据Dart的具体实现在一个独立的进程或是线程里运行,

那么我们需要多少个isolate?对于计算密集型的任务来说,我们应该根据有多少CPU可用就需要多少个isolate。如果只是单纯的计算,那么其它的那些isolate都是多余的。但是,如果isolate是异步执行,如执行IO,即它们不会花费CPU太多的时间,那么此时创建比CPU还多的isolate才有意义。

如果你的app有一个好的架构,那么当然可以使用超过CPU数量的isolate。例如为每一代码块使用独立的isolate,但也要保住它们之间没有数据共享。

测试一下

Q1

import 'dart:async';
void main() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 2'));

  new Future.delayed(new Duration(seconds:1),
                     () => print('future #1 (delayed)'));
  new Future(() => print('future #2 of 3'));
  new Future(() => print('future #3 of 3'));

  scheduleMicrotask(() => print('microtask #2 of 2'));

  print('main #2 of 2');
}
1234567891011121314

输出结果

main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)
1234567

代码有三个分支在执行:

  1. main()方法里的代码
  2. 微任务队列里的任务((scheduleMicrotask())
  3. 事件队列里的任务(new Future() 或 new Future.delayed())

要记住所有的代码在main方法里从开始到结束都是同步执行的。首先main()会调用print(),然后scheduleMicrotask(),然后new Future.delayed(),然后new Future()。之后就要按照回调里的参数以及它们各自的特性来执行了。

(microtask会优先于future执行,因为future属于event queue,被阻塞了;Future.delayed()由于delay因此会在new Future()后执行)

Q2

一个更复杂的例子:

import 'dart:async';
void main() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 3'));

  new Future.delayed(new Duration(seconds:1),
      () => print('future #1 (delayed)'));

  new Future(() => print('future #2 of 4'))
      .then((_) => print('future #2a'))
      .then((_) {
        print('future #2b');
        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
      })
      .then((_) => print('future #2c'));

  scheduleMicrotask(() => print('microtask #2 of 3'));

  new Future(() => print('future #3 of 4'))
      .then((_) => new Future(
                   () => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

  new Future(() => print('future #4 of 4'));
  scheduleMicrotask(() => print('microtask #3 of 3'));
  print('main #2 of 2');
}
123456789101112131415161718192021222324252627

输出结果

main #1 of 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
future #3 of 4
future #4 of 4
microtask #0 (from future #2b)
future #3a (a new future)
future #3b
future #1 (delayed)
123456789101112131415

像之前的,main()方法执行,然后是所有的微任务队列和所有的事件队列,这里面有一些点:

  • 当future 3里的then回调里调用了new Future(),意味着它创建了一个任务(#3a)到事件队列的末尾。

  • 当Future完成的时候,所有的then()回调会被执行,因此future2,2a,2b和2c会在让出控制劝之前就一次性执行。同样的3a和3b也是一样的道理。

  • 如果你将3a的代码从then((_) => new Future(...))改成then((_) {new Future(...); }),那么“future #3b”会出现的更早一些(在future #3之后,而不是future #3a)。

    (这应该是因为返回形式的不同,第一种直接 return Future , 那么后面的 then 就需要等这个 Future 执行完了,才会执行;而后者是在 then 回调了新建 new Future , 那么后面的 then 就跟这个 new Future 没关系了,它前面的 then 执行完了,这个 then 就会执行)

从这幅图我们就可以更清楚地看到future #2a、future #2b和future #2c其实都是属于同一个event里的

image