Dart异步任务与消息循环机制

Dart与消息循环机制

翻译自https://www.dartlang.org/articles/event-loop/java

异步任务在Dart中随处可见,例如许多库的方法调用都会返回Future对象来实现异步处理,咱们也能够注册Handler来响应一些事件,如:鼠标点击事件,I/O流结束和定时器到期。api

这篇文章主要介绍了Dart中与异步任务相关的消息循环机制,阅读完这篇文章后相信你可写出更赞的异步执行代码。你也能学习到如何调度Future任务而且预测他们的执行顺序。架构

在阅读这篇文章以前,你最好先要了解一下基本的Future用法app

基本概念

若是你写过一些关于UI的代码,你就应该熟悉消息循环和消息队列。有了他们才能保重UI的绘制操做和一些UI事件,如鼠标点击事件能够被一个一个的执行从而保证UI和UI事件的统一性。异步

<!-- more -->async

消息循环和消息队列

一个消息循环的职责就是不断从消息队列中取出消息并处理他们直到消息队列为空。
oop

消息队列中的消息可能来自用户输入,文件I/O消息,定时器等。例以下图的消息队列就包含了定时器消息和用户输入消息。
学习

上述的这些概念你可能已经得心应手了,那接下来咱们就讨论一下这些概念在Dart中是怎么表现的?测试

Dart的单线程执行

当一个Dart的方法开始执行时,他会一直执行直至达到这个方法的退出点。换句话说Dart的方法是不会被其余Dart代码打断的。spa

Note:一个Dart的命令行应用能够经过建立isolates来达到并行运行的目的。isolates之间不会共享内存,它们就像几个运行在不一样进程中的app,中能经过传递message来进行交流。出了明确指出运行在额外的isolates或者workers中的代码外,全部的应用代码都是运行在应用的main isolate中。要了解更多相关内容,能够查看https://www.dartlang.org/arti...

正以下图所示,当一个Dart应用开始的标志是它的main isolate执行了main方法。当main方法退出后,main isolate的线程就会去逐一处理消息队列中的消息。

事实上,上图是通过简化的流程。

Dart的消息循环和消息队列

一个Dart应用有一个消息循环和两个消息队列-- event队列microtask队列

event队列包含全部外来的事件:I/O,mouse events,drawing events,timers,isolate之间的message等。

microtask 队列在Dart中是必要的,由于有时候事件处理想要在稍后完成一些任务但又但愿是在执行下一个事件消息以前。

event队列包含Dart和来自系统其它位置的事件。但microtask队列只包含来自当前isolate的内部代码。

正以下面的流程图,当main方法退出后,event循环就开始它的工做。首先它会以FIFO的顺序执行micro task,当全部micro task执行完后它会从event 队列中取事件并执行。如此反复,直到两个队列都为空。

注意:当事件循环正在处理micro task的时候。event队列会被堵塞。这时候app就没法进行UI绘制,响应鼠标事件和I/O等事件

虽然你能够预测任务执行的顺序,但你没法准确的预测到事件循环什么时候会处理你指望的任务。例如当你建立一个延时1s的任务,但在排在你以前的任务结束前事件循环是不会处理这个延时任务的,也就是或任务执行多是大于1s的。

经过连接的方式指定任务顺序

若是你的代码之间存在依赖,那么尽可能让他们之间的依赖关系明确一点。明确的依赖关系能够很好的帮助其余开发者理解你的代码,而且可让你的代码更稳定也更容易重构。

先来看看下面这段错误代码:

// 这样写错误的缘由就是没有明确体现出设置变量和使用变量之间的依赖关系
future.then(...set an important variable...);
Timer.run(() {...use the important variable...});

正确的写法应该是:

// 明确表现出了后者依赖前者设置的变量值
future.then(...set an important variable...)
  .then((_) {...use the important variable...});

为了表示明确的先后依赖关系,咱们可使用then()()来代表要使用变量就必需要等设置完这个变量。这里可使用whenComplete()来代替then,它与then的不一样点在于哪怕设置变量出现了异常也会被调用到。这个有点像java中的finally。

若是上面这个使用变量也要花费一段时间,那么能够考虑将其放入一个新的Future中:

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

使用一个新的Future能够给事件循环一个机会先去处理列队中的其余事件。

怎么安排一个任务

当你须要指定一些代码稍后运行的时候,你可使用dart:async提供的两种方式:

1.Future类,它能够向event队列的尾部添加一个事件。
2.使用顶级方法**scheduleMicrotask()**,它能够向microtask队列的尾部添加一个微任务。

使用合理的队列

有可能的仍是尽可能使用Future来向event队列添加事件。使用event队列能够保持microtask队列的简短,以此减小microtask的过分使用致使event队列的堵塞。
若是一个任务确实要在event队列的任何一个事件前完成,那么你应该尽可能直接写在main方法中而不是使用这两个队列。若是你不能那么就用scheduleMicrotask来向microtask添加一个微任务。

Event队列

使用new Future或者new Future.delayed()来向event队列中添加事件。

注意:你也可使用Timer来安排任务,可是使用Timer的过程当中若是出现异常,则会退出程序。这里推荐使用Future,它是构建在Timer之上并加入了更多的功能,好比检测任务是否完成和异常反馈。

马上须要将任务加入event队列可使用new Future

//向event队列中添加一个任务
new Future(() {
  //任务具体代码
});

你也可使用then或者whenComplete在Future结束后马上执行某段代码。以下面这段代码在这个Future被执行后会马上输出42:

new Future(() => 21)
    .then((v) => v*2)
    .then((v) => print(v));

若是要在一段时间后添加一个任务,可使用new Future.delayed():

// 一秒之后将任务添加至event队列
new Future.delayed(const Duration(seconds:1), () {
  //任务具体代码
});

虽然上面这个例子中一秒后向event队列添加一个任务,可是这个任务想要被执行的话必须知足一下几点:

  1. main方法执行完毕

  2. microtask队列为空

  3. 该任务前的任务所有执行完毕

因此该任务真正被执行多是大于1秒后。

关于Future的有趣事实:

  1. 被添加到then()中的方法会在Future执行后立马执行(这方法没有被加入任何队列,只是被回调了)。

  2. 若是在then()调用以前Future就已经执行完毕了,那么会有一个任务被加入到microtask队列中。这个任务执行的就是被传入then的方法。

  3. Future()和Future.delayed()构造方法并不会被马上完成,他们会向event队列中添加一个任务。

    1. Future.value()构造方法会在一个microtask中完成。

    2. Future,sync()构造方法会立马执行其参数方法,并在microtask中完成。

Microtask队列: scheduleMicrotask()

dart:async定义了一个顶级方法scheduleMicrotask() ,你能够这样使用:

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

若是有必要可使用isolate或worker

若是你想要完成一些重量级的任务,为了保证你应用可响应,你应该将任务添加到isolate或者worker中。isolate可能会运行在不一样的进程或线程中.这取决于Dart的具体实现。

那通常状况下你应该使用多少个isolate来完成你的工做呢?一般状况下能够根据你的cpu的个数来决定。

但你也可使用超过cpu个数的isolate,前提是你的app能有一个好的架构。让不一样的isolate来分担不一样的代码块运行,但这前提是你能保证这些isolate之间没有数据的共享。

测试一下你的理解程度

目前为止你已经掌握了调度任务的基本知识,下面来测试一下你的理解程度。

问题1

下面这段代码的输出是什么?

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. microtask队列

  3. event队列(先new Future后new Future.delayed)

main方法中的普通代码都是同步执行的,因此确定是main打印先所有打印出来,等main方法结束后会开始检查microtask中是否有任务,如有则执行,执行完继续检查microtask,直到microtask列队为空。因此接着打印的应该是microtask的打印。最后会去执行event队列。因为有一个使用的delay方法,因此它的打印应该是在最后的。

问题2

下面这个问题相对有些复杂:

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)

总结

如下有几点关于dart的事件循环机制须要牢记于心:

  • Dart事件循环执行两个队列里的事件:event队列和microtask队列。

  • event队列的事件来自dart(future,timer,isolate message等)和系统(用户输入,I/O等)。

  • 目前为止,microtask队列的事件只来自dart。

  • 事件循环会优先清空microtask队列,而后才会去处理event队列。

  • 当两个队列都清空后,dart就会退出。

  • main方法,来自event队列和microtask队列的全部事件都运行在Dart的main isolate中。

当你要安排一个任务时,请遵照如下规则:

  • 若是能够,尽可能将任务放入event队列中。

    • 使用Future的then方法或whenComplete方法来指定任务顺序。

    • 为了保持你app的可响应性,尽可能不要将大计算量的任务放入这两个队列。

    • 大计算量的任务放入额外的isolate中。