Dart异步Future与事件循环Event Loop

更新日期: 2020-12-18阅读: 1.2k标签: Dart

如何使用异步Future

什么是异步

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

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

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

Dart异步处理库Future

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

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

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

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

Future的构造方法

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

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

Future类的构造方法如下

普通的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();
  }

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


事件循环基本概念

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

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

事件循环和队列

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


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


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

Dart的单线程

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

如下图所示,一个Dart程序开始执行的第一步是主isolate执行main()函数,当main()退出后,主isolate线程开始逐个处理程序事件队列上的所有事件。

 

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

dart的事件循环和队列

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

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

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

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

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

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


重要:当事件循环正在执行微任务队列中的任务时,事件队列会卡住:应用程序无法绘制图形、处理鼠标点击、对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()将这个任务添加到微任务队列中。


事件队列: 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://www.fly63.com/article/detial/9939

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!