Dart与消息循环机制

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

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

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

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

基本概念

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

消息循环和消息队列

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

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

上述的这些概念你可能已经驾轻就熟了,那接下来我们就讨论一下这些概念在Dart中是怎么表现的?

Dart的单线程执行

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

Note:一个Dart的命令行应用可以通过创建isolates来达到并行运行的目的。isolates之间不会共享内存,它们就像几个运行在不同进程中的app,中能通过传递message来进行交流。出了明确指出运行在额外的isolates或者workers中的代码外,所有的应用代码都是运行在应用的main isolate中。要了解更多相关内容,可以查看https://www.dartlang.org/articles/event-loop/#use-isolates-or-workers-if-necessary

正如下图所示,当一个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队列中添加一个任务。
  4. Future.value()构造方法会在一个microtask中完成。
  5. 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中。

Dart异步与消息循环机制的更多相关文章

  1. 【Dart学习】-- Dart之消息循环机制[翻译]

    概述 异步任务在Dart中随处可见,例如许多库的方法调用都会返回Future对象来实现异步处理,我们也可以注册Handler来响应一些事件,如:鼠标点击事件,I/O流结束和定时器到期. 这篇文章主要介 ...

  2. Win32消息循环机制等【转载】http://blog.csdn.net/u013777351/article/details/49522219

    Dos的过程驱动与Windows的事件驱动 在讲本程序的消息循环之前,我想先谈一下Dos与Windows驱动机制的区别: DOS程序主要使用顺序的,过程驱动的程序设计方法.顺序的,过程驱动的程序有一个 ...

  3. Android Handler 消息循环机制

    前言 一问起Android应用程序的入口,很多人会说是Activity中的onCreate方法,也有人说是ActivityThread中的静态main方法.因为Java虚拟机在运行的时候会自动加载指定 ...

  4. Android的消息循环机制 Looper Handler类分析

    Android的消息循环机制 Looper Handler类分析 Looper类说明   Looper 类用来为一个线程跑一个消息循环. 线程在默认情况下是没有消息循环与之关联的,Thread类在ru ...

  5. 安卓中的消息循环机制Handler及Looper详解

    我们知道安卓中的UI线程不是线程安全的,我们不能在UI线程中进行耗时操作,通常我们的做法是开启一个子线程在子线程中处理耗时操作,但是安卓规定不允许在子线程中进行UI的更新操作,通常我们会通过Handl ...

  6. Android HandlerThread 消息循环机制之源代码解析

    关于 HandlerThread 这个类.可能有些人眼睛一瞟,手指放在键盘上,然后就是一阵狂敲.立即就能敲出一段段华丽的代码: HandlerThread handlerThread = new Ha ...

  7. 理解Windows消息循环机制

    理解消息循环和整个消息传送机制对Windows编程十分重要.如果对消息处理的整个过程不了解,在windows编程中会遇到很多令人困惑的地方. 什么是消息(Message)每个消息是一个整型数值,如果查 ...

  8. 详谈Windows消息循环机制

    一直对windows消息循环不太清楚,今天做个详细的总结,有说错的地方,请务必指出. 用VS2017新建一个win32 Application的默认代码如下: 程序入口                ...

  9. Chromium on Android: Android在系统Chromium为了实现主消息循环分析

    总结:刚开始接触一个Chromium on Android时间.很好奇Chromium主消息循环是如何整合Android应用. 为Android计划,一旦启动,主线程将具有Java消息层循环处理系统事 ...

随机推荐

  1. Unity3D 应用程序退出调用OnDestroy测试

    测试结果为关闭游戏,会调用OnDestroy().但OnApplicationQuit()比它提前调. using UnityEngine; using System.Collections; pub ...

  2. Atitit. 构造ast 语法树的总结attilax oao

    Atitit. 构造ast 语法树的总结attilax oao 1. 能那更加有意义的名字来命名ast节点... 1 2. 如何命名表达式名称..使用实际对象名称,而不是操作符号表达式更好 1 2.1 ...

  3. Android Bitmap和Canvas学习笔记

    位图是我们开发中最常用的资源,毕竟一个漂亮的界面对用户是最有吸引力的. 1. 从资源中获取位图 可以使用BitmapDrawable或者BitmapFactory来获取资源中的位图. 当然,首先需要获 ...

  4. css 盒子垂直居中

    面试的时候经常会被问到这样一个题目:让一个元素中内容垂直居中怎么做.其实之前,我就会两种,line-height和table-cell,今天做项目,遇到了这个问题,就系统的查了一下,总结一下方法: 1 ...

  5. CSS3边框圆角知识

    <div class="item" data-brief="整圆"> <div class="border-radius" ...

  6. 一款基于HTML5 Canvas的画板涂鸦动画

    今天给各网友分享一款基于HTML5 Canvas的画板涂鸦动画.记得之前我们分享过一款HTML5 Canvas画板工具,可以切换不同的笔刷,功能十分强大.本文今天要再来分享一款基于HTML5 Canv ...

  7. Android之SystemUI载入流程和NavigationBar的分析

    Android之SystemUI载入流程和NavigationBar的分析 本篇仅仅分析SystemUI的载入过程和SystemUI的当中的一个模块StatusBar的小模块NavigationBar ...

  8. kickstart安装步骤

    1.1 环境说明 [root@test ~]# cat /etc/redhat-release CentOS release 6.9 (Final) [root@test ~]# uname -r 2 ...

  9. Entity Framework(四):使用DbModelBuilder API创建表结构

    DbContext类有一个OnModelCreating方法,它用于流利地配置领域类到数据库模式的映射.下面我们以fluent API的方式来定义映射.首先,先将Product类注释掉,重新编写该类, ...

  10. 微信APP支付 - C#

    最近挺忙的,没时间写东西.然后在弄微信APP支付,网上的搜索一趟,都比较凌乱,我也遇到一些坑,不过也算弄好了,记录分享一下. 1.准备各种调用接口需要的参数,配置app.config. <!-- ...