Dart异步Future与事件循环Event Loop

如何使用异步Future

什么是异步

若是你的程序中有两个方法,这两个方法桉顺序执行,第一个方法执行须要五秒,若是是同步代码,第二个方法会等待第一个方法执行完,才会被调用,html

若是第一个方法是异步的,程序在执行第一个方法时,不会等待它执行结束,而是接着执行第二个方法,这样第二个方法就无需在第一个方法执行完以后被调用。git

在客户端异步是很是有用的,若是你在初始化时有一个很是耗时,但又不须要它在ui画面响应前执行完成的方法,你就可使用异步。github

Dart异步处理库Future

了解了异步的概念后,咱们来看一看如何在Dart中使用异步。web

testFuture();
    testFuture2();
    
  Future testFuture() {
    //下面是一个耗时三秒的任务
    return Future.delayed(Duration(seconds: 3), () => print('异步方法'));
  }

  testFuture2() {
    print("普通方法");
  }

控制台输出api

在这里插入图片描述

将一个方法的返回值声明为Future这样这个方法就是异步的了。浏览器

Future的构造方法

你也可使用Future类的构造方法来使用异步架构

Future(() {
      print('异步方法');
    });

Future类的构造方法以下app

普通的Future类构造
Future(FutureOr<T> computation())

建立一个延迟几秒执行的Future  duration参数来控制延迟多久
Future.delayed(Duration duration, [FutureOr<T> computation()])

经过微任务队列处理的Future
Future.microtask(FutureOr<T> computation())

当即返回结果的Future
Future.sync(FutureOr<T> computation())

Future.value([FutureOr<T>? value])

Future.error(Object error, [StackTrace? stackTrace])

构造方法演示框架

Future.delayed(Duration(seconds: 3), () => print('异步方法1'));
Future(() {
  print('异步方法2');
});
Future.microtask(() => print('异步方法3'));
Future.sync(() => print('异步方法4'));

控制台输出以下异步

在这里插入图片描述

看到不一样构造方法的执行顺序,想必你已经对不一样的构造方法有所了解

值得一提的是Future全部的构造方法返回的都是Future对象,咱们能够进行链式调用

Future的链式调用

当 future 执行完成后,then() 中的代码会被执行。

Future(() {
  print('异步方法');
}).then((value) => print('异步方法2'));

等待多个 Future

有时代码逻辑须要调用多个异步函数, 并等待它们所有完成后再继续执行。 使用 Future.wait() 静态方法管理多个 Future 以及等待它们完成:

Future deleteLotsOfFiles() async =>  ...
Future copyLotsOfFiles() async =>  ...
Future checksumLotsOfOtherFiles() async =>  ...

 Future.wait([
  deleteLotsOfFiles(),
  copyLotsOfFiles(),
  checksumLotsOfOtherFiles(),
]);

简化Future

使用async和awiat来简化异步代码

这样声明一个方法 它就是异步的了

testFuture5() async {
    Future.delayed(Duration(seconds: 3), () => print('异步方法1'));
  }

你也能够像写同步代码同样使用异步,当在async声明的方法中使用await时,async声明的方法会等待await修饰的方法执行结束

testFuture2() {
    print("普通方法");
  }
  
  testFuture5() async {
   await Future.delayed(Duration(seconds: 3), () => print('异步方法1'));
  }

  test()async{
   await testFuture5();
    testFuture2();
  }

控制台输出以下

image-20201213154650011

若是你对以上的默写代码执行顺序有所疑惑,不要着急,下面的内容会解答你的全部问题。

事件循环基本概念

本文描述了Dart的事件循环架构,您就能够编写出更好的更少问题的异步代码。您将学习如何使用Future,而且可以预测程序的执行顺序。

若是你写过UI代码,你可能已经熟悉了事件循环和事件队列的概念。它们确保了图形操做和事件(如鼠标点击)一次只处理一个。

事件循环和队列

事件循环的工做是从事件队列中获取一个事件并处理它,只要队列中有事件,就重复这两个步骤。

events going into a queue, feeding into an event loop

队列中的事件可能表明用户输入,文件I / O通知,计时器等。 例如,下面是事件队列的图片,其中包含计时器和用户输入事件:

same figure, but with explicit events: 1. key, 2.click, 3. timer, etc.

你可能在其余的语言中熟悉这些。如今咱们来谈谈dart语言是如何实现的。

Dart的单线程

一旦一个Dart函数开始执行,它将继续执行直到退出。换句话说,Dart函数不能被其余Dart代码打断。

以下图所示,一个Dart程序开始执行的第一步是主isolate执行main()函数,当main()退出后,主isolate线程开始逐个处理程序事件队列上的全部事件。 在这里插入图片描述

实际上,这有点过于简化了。

dart的事件循环和队列

Dart应用程序的事件循环带有两个队列——事件队列和微任务队列。

事件队列包含全部外部事件:I/O、鼠标事件、绘图事件、计时器、Dart isolate之间的通讯,等等。

微任务队列是必要的,由于事件处理代码有时须要稍后完成一个任务,但在将控制权返回到事件循环以前。例如,当一个可观察对象发生变化时,它将几个突变变化组合在一块儿,并同步地报告它们。微任务队列容许可观察对象在DOM显示不一致状态以前报告这些突变变化。

事件队列包含来自应用程序中的事件,微任务队列只包含来自Dart核心代码的事件。

以下图所示,当main()函数退出时,事件循环开始工做。首先,它以FIFO(先进先出)顺序执行全部微任务。而后,它使事件队列中的第一项出队并处理,而后它重复这个循环:执行全部微任务,而后处理事件队列上的下一事件。一旦两个队列都为空而且不会再发生任何事件,应用程序的嵌入程序(如浏览器或测试框架)就能够释放应用程序。

<u>注意:若是web应用程序的用户关闭了它的窗口,那么web应用程序可能会在其事件队列为空以前强行退出。</u>

flowchart: main() -> microtasks -> next event -> microtasks -> ...

重要:当事件循环正在执行微任务队列中的任务时,事件队列会卡住:应用程序没法绘制图形、处理鼠标点击、对I/O作出反应等。

尽管能够预测任务执行的顺序,但不能准确预测事件循环什么时候将任务从队列中移除。Dart事件处理系统基于单线程循环;它不是基于任何类型的时间标准。例如,当您建立一个延迟的任务时,事件将在您指定的时间进入队列。他仍是要等待事件队列中它以前的全部事件(包括微任务队列中的每个事件)所有执行完后,才能获得执行。(延时任务不是插队,是在指定时间进入队列)

提示:链式调用future指定任务顺序

若是您的代码有依赖关系,请以显式的方式编写。显式依赖关系帮助其余开发人员理解您的代码,而且使您的程序能方便的重构。

下面是一个错误编码方式的例子:

// 由于在设置变量和使用变量之间没有明确的依赖关系,因此很差。
future.then((){...设置一个重要变量...)。
Timer.run(() {...使用重要变量...})。

相反,像这样写代码:

//更好,由于依赖关系是显式的。

future.then(…设置一个重要的变量…)

then((_){…使用重要的变量…});

在使用该变量以前必须先设置它。(若是您但愿即便出现错误也能执行代码,那么可使用whenComplete()而不是then()。)

若是使用变量须要时间而且能够在之后完成,请考虑将代码放在新的Future中:

//可能更好:显式依赖加上延迟执行。

future.then(…设置一个重要的变量…)

then((_) {new Future((){…使用重要的变量…})});

使用新的Future使事件循环有机会处理事件队列中的其余事件。下一节将详细介绍延迟运行的调度代码。

如何安排任务

当您须要指定一些须要延迟执行的代码时,可使用dart:async库提供的如下API:

Future类,它将一个项目添加到事件队列的末尾。

顶级的scheduleMicrotask()函数,它将一个项目添加到微任务队列的末尾。

使用这些api的示例在下一节中。事件队列:new Future()和微任务队列:scheduleMicrotask()

使用适当的队列(一般是事件队列)

尽量的在事件队列上调度任务,使用Future。使用事件队列有助于保持微任务队列较短,减小微任务队列影响事件队列的可能。

若是一个任务须要在处理任何来自事件队列的事件以前完成,那么你一般应该先执行该函数。若是不能先执行,那么使用 scheduleMicrotask()将这个任务添加到微任务队列中。

shows chain of event handler execution, with tasks added using Future and scheduleMicrotask().

事件队列: new Future()

要在事件队列上调度任务,可使用new Future()或new Future.delayed()。这是dart:async库中定义的两个Future的构造函数。

注意:您也可使用Timer安排任务,可是若是Timer任务中发生任何未捕获的异常,您的应用程序将退出。 相反,咱们建议使用Future,它创建在Timer之上,并增长了诸如检测任务完成和对错误进行响应的功能。

要当即将一个事件放到事件队列中,使用new Future():

//在事件队列中添加任务。

new Future((){

 /……代码就在这里……

});

您能够添加对then()或whenComplete()的调用,以便在新的Future完成后当即执行一些代码。例如,当new Future的任务离开队列时,如下代码输出“42”:

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

使用new Future.delayed()在一段时间后在队列中加入一个事件:

// 一段时间以后,将事件加入队列
new Future.delayed(const Duration(seconds:1), () {
  // ...代码在这里...
});

尽管前面的示例在一秒后将任务添加到事件队列中,但该任务只有在主isolate空闲、微任务队列为空以及以前在事件队列中入队的任务所有执行完后才能执行。例如,若是main()函数或事件处理程序正在运行一个复杂的计算,则任务只有在该计算完成后才能执行。在这种状况下,延迟可能远不止一秒。

关于future的重要细节:

1 传递给Future的then()方法的函数在Future完成时当即执行。(函数没有进入队列,只是被调用了)

2 若是Future在调用then()以前已经完成,则将一个任务添加到微任务队列,而后该任务执行传递给then()的函数。

3 Future()和Future.delayed()构造函数不会当即完成; 他们将一个项目添加到事件队列。

4 value()构造函数在微任务中完成,相似于#2

5 Future.sync()构造函数当即执行其函数参数,而且(除非该函数返回Future,若是返回future代码会进入事件队列)在微任务中完成,相似于#2。(Future.sync(FutureOr<T> computation())该函数接受一个function参数)

微任务队列:scheduleMicrotask()

async库将scheduleMicrotask()定义为一个顶级函数。你能够像这样调用scheduleMicrotask():

scheduleMicrotask(() {
  // ...代码在这里...
});

dart2.9会将第一次调用scheduleMicrotask()时,将此代码插入事件队列的第一位

向微任务队列添加任务的一种方法是在已经完成的Future上调用then()。有关更多信息,请参阅前一节

必要时使用isolates 或workers

如今您已经阅读了关于调度任务的全部内容,让咱们测试一下您的理解。

请记住,您不该该依赖Dart的事件队列实现来指定任务顺序。 实现可能会发生变化,Future的then()和whenComplete()方法是更好的选择。 不过,若是您能正确回答下面这些问题,你学会了。

练习

Question #1

这个示例打印出什么?

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

答案

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()函数中的全部调用都是从头至尾同步执行的。首先main()调用print(),而后调用scheduleMicrotask(),再调用new Future.delayed(),而后调用new Future(),以此类推。只有回调--做为 scheduleMicrotask()、new Future.delayed()和new Future()的参数代码才会在后面的时间执行

Question #2

这里有一个更复杂的例子。若是您可以正确地预测这段代码的输出,就会获得一个闪亮的星星。

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

在这里插入图片描述

dart程序会在第一次建立微任务队列时,将建立微任务队列的代码插入到事件队列的第一位,至关于插队。

总结

你如今应该了解Dart的事件循环以及dart如何安排任务。如下是Dart中事件循环的一些主要概念:

Dart应用程序的事件循环使用两个队列执行任务:事件队列和微任务队列。

事件队列有来自Dart(futures、计时器、isolate messages)和系统(用户操做、I/O等)的事件。

目前,微任务队列只有来自Dart核心代码的事件,若是你想让你的代码进入微任务队列执行,使用scheduleMicrotask()。

事件循环在退出队列并处理事件队列上的下一项以前先清空微任务队列。

一旦两个队列都为空,应用程序就完成了它的工做,而且(取决于它的嵌入程序)能够退出。

main()函数和来自微任务和事件队列的全部项目都运行在Dart应用程序的主isolates 上。

当你安排一项事件时,遵循如下规则:

若是可能,将其放在事件队列中(使用new Future()或new Future.delayed())。

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

为了不耗尽事件循环,请保持微任务队列尽量短。

为了保持应用程序的响应性,避免在任何一个事件循环中执行计算密集型任务。

要执行计算密集型任务,请建立额外的isolates 或者 workers。

参考文章: https://dart.cn/articles/archive/event-loop#:~:text=A%20Dart%20app%20has%20a,queue%20and%20the%20microtask%20queue.&text=First,%20it%20executes%20any%20microtasks,item%20on%20the%20event%20queue.

https://api.dart.dev/stable/2.10.4/dart-async/Future-class.html

https://www.dartcn.com/guides/libraries/library-tour#future

文章中全部的测试异步的代码都在做者的github上,https://github.com/jack0-0wu/flutter_demo。

若是你对Dart flutter 计算机基础感兴趣能够关注做者,持续分享优质文章。 坐而论道不如起而行之