Dart学习笔记(30):Event Loop事件循环

发表于2018-07-04 13:20 阅读(120)

目录
1 基本概念
2 如何调度任务
….2.1 事件队列:new Future()
….2.2 微任务队列:scheduleMicrotask()
3 练习测试

本文地址:http://www.cndartlang.com/890.html

本文将介绍Dart中的事件循环体系,你会从中了解到如何调度Future任务,以及预测任务执行的顺序,以便更好地编写异步代码,减少意外情况的出现。

1、基本概念

事件循环的任务是从事件队列中一次取出一项并处理,并且只要队列有内容,就会重复这两步工作。消息队列的内容可能是用户输入、文件I/O通知、定时器等等。而你可能是从其他的非Dart语言中,所了解熟悉的这些内容:

在Dart中,略有区别。Dart应用程序在Main Isolate执行main函数之后开始运行,当main函数执行完毕退出后,Main Isolate的线程开始一个个的处理应用程序的事件队列中的内容。下图是简化的流程:

Dart应用程序仅有一个消息循环,以及两个队列:event事件队列和microtask微任务队列。事件队列包含所有的外部事件,如:I/O、鼠标事件、定时器、Isolate之间的消息等等。微任务队列是非常有必要的,因为在返回操作到事件循环前,事件处理代码有时候需要完成某个工作任务。例如,当一个可观察的对象发生改变的时候,它会将多个变化组织到一起,以异步的方式提交报告。微任务队列允许可观察对象在DOM能够显示不一致状态之前,提交改变。

事件队列包含Dart和系统产生的事件。目前,微任务队列的入口点仅来自Dart代码内部。如下图显示,当main函数exit后,事件循环开始工作。首先,按FIFO(先进先出)的方式执行所有微任务。接着,从事件队列中提取消息并一条条处理。然后再执行所有的微任务,以此循环,直到所有队列为空,没有新预计的事件发生为止。(如果Web应用的用户关闭窗口,Web应用可能在队列清空之前退出)

需要注意,当事件循环在处理微任务队列的时候,事件队列会被卡住,应用程序无法处理鼠标单击、I/O消息等事件。

虽然可以预测任务执行的顺序,但是我们无法预测事件循环什么时候会从队列中提取任务。Dart事件处理系统基于单线程循环,而不是基于时基(tick,系统的相对时间单位)或者其他的时间度量。例如,当你创建一个延迟任务的时候,事件会在你指定的时间插入到队列末尾,并且在队列中之前的事件未处理之前,会一直排队等待,微任务队列同样如此。

通过Future链指定任务执行顺序

如果你的代码有依赖关系,请显式地指定、明确他们执行的顺序,而不是依赖事件队列的顺序,避免出现意料之外的情况,因为Dart中事件队列的实现有可能发生变化。同时,也使其他开发者更容易理解你的代码。

下面是一段错误的代码:

// 错误,因为并没有明确设置和使用变量之间的依赖关系
future.then(...set an important variable...);
Timer.run(() {...use the important variable...});

应该用下面的方式代替,then()是更好的选择:

future.then(...set an important variable...)
  .then((_) {...use the important variable...});

如果你想执行代码,即使发生错误的时候,可以使用whenComplete()代替then()。如果使用变量比较耗时,可以考虑将代码放到新的Future中。

future.then(...set an important variable...)
  .then((_) {new Future(() {...use the important variable...})});

上面的代码使用新的Future来使用变量,给事件循环一个机会来处理事件队列中的其他事件。下一节会详述延迟运行调度代码的内容。

2、如何调度任务

当你需要指定稍后执行某段代码,可以使用dart:async包中提供的如下API:
1、Future类,可添加一个事件到事件队列的末尾;
2、顶层函数 scheduleMicrotask(),可添加一个任务到微任务队列的末尾。

一般情况下,我们通过Future来使用事件队列来调度任务。因为要处理完微任务队列之后才会处理事件队列,所以尽量使用事件队列可以使微任务队列更短,降低事件队列卡死的可能性。

如果一个任务必须在所有的事件队列之前处理,你应该立即执行该任务。如果不能立即执行,可以使用scheduleMicrotask()将代码添加到微任务队列。例如,在Web应用中,在事件队列之前执行微任务,来避免过早地释放或结束一个IndexedDB事务处理等等。

2.1 事件队列:new Future()

插入事件队列可以使用new Future()或new Future.delayed(),这是dart:async中定义的两个Future的构造函数。

提示:你也可以Timer来调度任务,但是如果出现未捕获的异常,应用程序会立即退出。当然,更建议的是使用Future。Future是在Timer的基础上构建的,并添加了检测任务完成、响应错误异常等功能。

立即将一条内容添加到事件队列,使用new Future():

// Adds a task to the event queue.
new Future(() {
  // ...code goes here...
});

在之后的某一时间将内容加入事件队列,使用new Future.delayed():

// After a one-second delay, adds a task to the event queue.
new Future.delayed(const Duration(seconds:1), () {
  // ...code goes here...
});

虽然上面的代码在1s后将任务添加到了事件队列,但要等到main Isolate为空闲状态,微任务队列为空,且事件队列之前的任务执行完毕之后,该任务才会执行。例如,如果main函数或事件处理程序正在执行一个耗时的计算,任务并不会立即执行,直到计算完成。在这种情况下,延迟事件可能远远超过1s。

提示:如果你在Web应用中为动画绘制帧,不要使用Future、Timer或Stream。而是使用animationFrame,这是requestAnimationFrame的Dart接口。

Future几点注意事项:
1、当Future完成计算后,then()注册的回调函数会立即执行。需注意的是,then()注册的函数并不会添加到事件队列中,回调函数只是在事件循环中任务完成后被调用。
2、如果Future在then()被调用之前已经完成计算,那么任务会被添加到微任务队列中,并且该任务会执行then()中注册的回调函数。
3、Future()和Future.delayed()构造函数并不会立即完成计算。
4、Future.value()构造函数在微任务中完成计算,其他类似第2条。
5、Future.sync()构造函数会立即执行函数,并在微任务中完成计算,其他类似第2条。

关于第2条,可以参考下面的代码:

import 'dart:async';

main() async {
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => null);

  f3.then((_) => print("f3 then"));

  f2.then((_) {
    print("f2 then");    
    new Future(() => print("new Future befor f1 then"));
    f1.then((_) {
      print("f1 then");
    });
  });
}

上面的代码中new Future(() => null)和new Future(null)有本质上的区别,一个上函数体为空,什么都不做,一个是参数为空,不存在函数。

运行结果:

f2 then
f1 then
f3 then
new Future befor f1 then

首先,f1、f2和f3会将任务添加到事件队列中,而且then()注册的函数并不会被添加到队列,也不会直接运行。接着完成计算,在f2.then中,new Future会将任务添加到事件队列,f1因为已经完成计算,因此f3.then会将任务添加到微任务队列,先于new Future打印信息。

2.2 微任务队列:scheduleMicrotask()

scheduleMicrotask()是dart:async中定义的顶层函数,可以调用如下:

scheduleMicrotask(() {
  // ...code goes here...
});

如果有必要的话,使用Isolate和Worker

要是你有一个计算密集型的任务需要运行怎么办?为了使应用程序保持响应,应该将任务放入Isolate或Worker中。Isolate可能运行在一个单独的进程或线程中,这取决于Dart的具体实现。并且,你可以在Dart Web应用中使用dart:html的Worker来添加一个JavaScript worker。

那么应该使用多少Isolate隔离区?对于计算密集型任务,一般隔离区的数量取决于你CPU有多少可用。如果只是纯粹地计算,很多额外的隔离区只是浪费资源。然而,如果隔离区执行异步调用,如执行I/O操作,并不会花费很多的时间在CPU上,那么比CPU更多的隔离区也是合情合理的。你可以使用比CPU数量更多的隔离区,如果你的应用有一个完善良好的体系结构。例如,你可能会为每一个功能创建一个单独的隔离区,或当你确保不需要共享数据的时候。

3、练习测试

练习一:下面代码的输出是什么?

import 'dart:async';
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');
}

运行结果:

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)

上面的代码分三个顺序执行:
1、main()函数中的代码
2、微任务队列中的任务(scheduleMicrotask())
3、事件队列中的任务(new Future() 或 new Future.delayed())

注:在main()函数中,从开始到结束的调用都是同步模式,其中第一个调用的是print(),然后是scheduleMicrotask()、new Future.delayed()、new Future()……而scheduleMicrotask()、Future.delayed()、new Future()中作为参数的回调函数,会延迟调用。

练习二:接下来是一个稍复杂的例子,如果你能正确的预测代码的输出,奖励一朵小红花!

import 'dart:async';

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');
}

运行结果:

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
microtask #0 (from future #2b)
future #3 of 4
future #4 of 4
future #3a (a new future)
future #3b
future #1 (delayed)

就像之前说的,main()函数先执行,然后是微任务队列,以及事件队列。下面是3个要注意的地方:
1、Future 3的then函数中,回调函数返回新的Future,它将创建一个新任务#3a,并添加到事件队列中,因为之后的then执行的前提是Future完成计算,由于这时候微任务队列为空,因此#3a、#3b一次性执行完。
2、所有then()注册的回调函数,当Future完成后立即调用执行,因此Future 2、2a、2b、2c在返回控制权前一次性执行完。
3、如果将Future 3中then的代码由.then((_) => new Future( () => print(‘future #3a (a new future)’)))改为.then((_){ new Future( () => print(‘future #3a (a new future)’));})因为没有return语句,这时候回调函数返回的是Null Future,Future 3a会添加到事件队列,而不会立即执行。

下面是代码所属队列的图示:

总结

当你调度任务的时候,尽量遵循下列规则:

  • 尽量使用事件队列(new Future() 或new Future.delayed())
  • 当需要指定任务顺序的时候,使用Future.then()或whenComplete()
  • 为避免事件队列“饥饿锁死”,尽量使微任务简短
  • 为使应用程序保持响应,避免在事件循环中添加计算密集型代码
  • 执行计算密集型代码的时候,另创建Isolate或Worker