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. 连接SQLServer的增删改查方法代码

    在Visual C++中用ADO进行数据库编程 1. 生成应用程序框架并初始化OLE/COM库环境 创建一个标准的MFC AppWizard(exe)应用程序CADOConnection,然后在使用A ...

  2. 模拟元素的title属性,自定义Vue指令

    function showTitle(el, title) { const popover = getPopover() const popoverStyle = popover.style if ( ...

  3. HTML5 + AJAX ( jQuery版本 ) 文件上传带进度条

    页面技术:HTML5 + AJAX ( jQuery) 后台技术:Servlet 3.0 服务器:Tomcat 7.0 jQuery版本:1.9.1 Servlet 3.0 代码 package or ...

  4. 配置ubuntu - tftp server服务器步骤

    配置Ubuntu tftp服务的步骤: 1.安装相关软件包:Ubuntu tftp(服务端),tftp(客户端),xinetd sudo apt-get install tftpd tftp xine ...

  5. 扩展KMP - HDU 4333 Revolving Digits

    Revolving Digits Problem's Link Mean: 给你一个字符串,你可以将该字符串的任意长度后缀截取下来然后接到最前面,让你统计所有新串中有多少种字典序小于.等于.大于原串. ...

  6. EasyUI Window和Layout

    我们建立tabs内容. <div class="easyui-window" title="Layout Window" icon="icon- ...

  7. 超简单CSS3实现圆角、阴影、透明效果

    CSS实现圆角,阴影,透明的方法很多,传统的方法都比较复杂,用CSS3就方便很多了,虽然现在各浏览器对CSS3的支持还不是很好,但不久的将来CSS3就会普及. 1.圆角 CSS3实现圆角有两种方法. ...

  8. NodeJS与Javascript时代

    如果你一直在关注互联网的相关技术,你应该会有这样一种感觉,web技术正在发生着变革,虽然我们不愿相信,但一个事实已经越来越清晰的摆在了眼前:LAMP组合的时代将要成为历史,在web诞生的二十年间,它影 ...

  9. [android] AndroidManifest.xml - 【 manifest -> permission】

    在  API Level 1 时被引入 语法: <permission android:description="string resource" android:icon= ...

  10. 游戏开发之coco2dx ---2d 游戏特效

    http://www.cnblogs.com/gamedes/p/4547722.html