[译] Flutter 异步编程:Future、Isolate 和事件循环

本文介绍了 Flutter 中不一样的代码执行模式:单线程、多线程、同步和异步。html

难度:中级前端

概要

我最近收到了一些与 FutureasyncawaitIsolate 以及并行执行概念相关的一些问题。android

因为这些问题,一些人在处理代码的执行顺序方面遇到了麻烦。ios

我认为经过一篇文章来解释异步并行处理这些概念并消除其中任何歧义是很是有用的。git


Dart 是一种单线程语言

首先,你们须要牢记,Dart单线程的而且 Flutter 依赖于 Dartgithub

重点编程

Dart 同一时刻只执行一个操做,其余操做在该操做以后执行,这意味着只要一个操做正在执行,它就不会被其余 Dart 代码中断。后端

也就是说,若是你考虑纯粹的同步方法,那么在它完成以前,后者将是惟一要执行的方法。api

void myBigLoop(){
    for (int i = 0; i < 1000000; i++){
        _doSomethingSynchronously();
    }
}
复制代码

在上面的例子中,myBigLoop() 方法在执行完成前永远不会被中断。所以,若是该方法须要一些时间,那么在整个方法执行期间应用将会被阻塞数组


Dart 执行模型

那么在幕后,Dart 是如何管理操做序列的执行的呢?

为了回答这个问题,咱们须要看一下 Dart 的代码序列器(事件循环)。

当你启动一个 Flutter(或任何 Dart)应用时,将建立并启动一个新的线程进程(在 Dart 中为 「Isolate」)。该线程将是你在整个应用中惟一须要关注的。

因此,此线程建立后,Dart 会自动:

  1. 初始化 2 个 FIFO(先进先出)队列(「MicroTask」和 「Event」);
  2. 而且当该方法执行完成后,执行 main() 方法,
  3. 启动事件循环

在该线程的整个生命周期中,一个被称为事件循环单一且隐藏的进程将决定你代码的执行方式及顺序(取决于 MicroTaskEvent 队列)。

事件循环是一种无限循环(由一个内部时钟控制),在每一个时钟周期内若是没有其余 Dart 代码执行,则执行如下操做:

void eventLoop(){
    while (microTaskQueue.isNotEmpty){
        fetchFirstMicroTaskFromQueue();
        executeThisMicroTask();
        return;
    }

    if (eventQueue.isNotEmpty){
        fetchFirstEventFromQueue();
        executeThisEventRelatedCode();
    }
}
复制代码

正如咱们看到的,MicroTask 队列优先于 Event 队列,那这 2 个队列的做用是什么呢?

MicroTask 队列

MicroTask 队列用于很是简短且须要异步执行的内部动做,这些动做须要在其余事情完成以后并在将执行权送还给 Event 队列以前运行。

做为 MicroTask 的一个例子,你能够设想必须在资源关闭后当即释放它。因为关闭过程可能须要一些时间才能完成,你能够按照如下方式编写代码:

MyResource myResource;

...

void closeAndRelease() {
    scheduleMicroTask(_dispose);
    _close();
}

void _close(){
    // 代码以同步的方式运行
    // 以关闭资源
    ...
}

void _dispose(){
    // 代码在
    // _close() 方法
    // 完成后执行
}
复制代码

这是大多数时候你没必要使用的东西。好比,在整个 Flutter 源代码中 scheduleMicroTask() 方法仅被引用了 7 次。

最好优先考虑使用 Event 队列。

Event 队列

Event 队列适用于如下参考模型

  • 外部事件如
    • I/O;
    • 手势;
    • 绘图;
    • 计时器;
    • 流;
    • ……
  • futures

事实上,每次外部事件被触发时,要执行的代码都会被 Event 队列所引用。

一旦没有任何 micro task 运行,事件循环将考虑 Event 队列中的第一项并执行它。

值得注意的是,Future 操做也经过 Event 队列处理。


Future

Future 是一个异步执行而且在将来的某一个时刻完成(或失败)的任务

当你实例化一个 Future 时:

  • Future 的一个实例被建立并记录在由 Dart 管理的内部数组中;
  • 须要由此 Future 执行的代码直接推送到 Event 队列中去;
  • future 实例 返回一个状态(= incomplete);
  • 若是存在下一个同步代码,执行它(非 Future 的执行代码

只要事件循环Event 循环中获取它,被 Future 引用的代码将像其余任何 Event 同样执行。

当该代码将被执行并将完成(或失败)时,then()catchError() 方法将直接被触发。

为了说明这一点,咱们来看下面的例子:

void main(){
    print('Before the Future');
    Future((){
        print('Running the Future');
    }).then((_){
        print('Future is complete');
    });
    print('After the Future');
}
复制代码

若是咱们运行该代码,输出将以下所示:

Before the Future
After the Future
Running the Future
Future is complete
复制代码

这是彻底正确的,由于执行流程以下:

  1. print(‘Before the Future’)
  2. (){print(‘Running the Future’);} 添加到 Event 队列;
  3. print(‘After the Future’)
  4. 事件循环获取(在第二步引用的)代码并执行它
  5. 当代码执行时,它会查找 then() 语句并执行它

须要记住一些很是重要的事情:

Future 并不是并行执行,而是遵循事件循环处理事件的顺序规则执行。


Async 方法

当你使用 async 关键字做为方法声明的后缀时,Dart 会将其理解为:

  • 该方法的返回值是一个 Future
  • 同步执行该方法的代码直到第一个 await 关键字,而后它暂停该方法其余部分的执行;
  • 一旦由 await 关键字引用的 Future 执行完成,下一行代码将当即执行。

了解这一点是很是重要的,由于不少开发者认为 await 暂停了整个流程直到它执行完成,但事实并不是如此。他们忘记了事件循环的运做模式……

为了更好地进行说明,让咱们经过如下示例并尝试指出其运行的结果。

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  Future((){                // <== 该代码将在将来的某个时间段执行
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

  print('C end from $from');
}

methodD(){
  print('D');
}
复制代码

正确的顺序是:

  1. A
  2. B start
  3. C start from B
  4. C end from B
  5. B end
  6. C start from main
  7. C end from main
  8. D
  9. C running Future from B
  10. C end of Future from B
  11. C running Future from main
  12. C end of Future from main

如今,让咱们认为上述代码中的 methodC() 为对服务端的调用,这可能须要不均匀的时间来进行响应。我相信能够很明确地说,预测确切的执行流程可能变得很是困难。

若是你最初但愿示例代码中仅在全部代码末尾执行 methodD() ,那么你应该按照如下方式编写代码:

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  await Future((){                  // <== 在此处进行修改
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });
  print('C end from $from');
}

methodD(){
  print('D');
}
复制代码

输出序列为:

  1. A
  2. B start
  3. C start from B
  4. C running Future from B
  5. C end of Future from B
  6. C end from B
  7. B end
  8. C start from main
  9. C running Future from main
  10. C end of Future from main
  11. C end from main
  12. D

事实是经过在 methodC() 中定义 Future 的地方简单地添加 await 会改变整个行为。

另外,需特别谨记:

async 并不是并行执行,也是遵循事件循环处理事件的顺序规则执行。

我想向你演示的最后一个例子以下。 运行 method1method2 的输出是什么?它们会是同样的吗?

void method1(){
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  myArray.forEach((String value) async {
    await delayedPrint(value);
  });
  print('end of loop');
}

void method2() async {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  for(int i=0; i<myArray.length; i++) {
    await delayedPrint(myArray[i]);
  }
  print('end of loop');
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print('delayedPrint: $value');
}
复制代码

答案:

method1() method2()
1. before loop 1. before loop
2. end of loop 2. delayedPrint: a (after 1 second)
3. delayedPrint: a (after 1 second) 3. delayedPrint: b (1 second later)
4. delayedPrint: b (directly after) 4. delayedPrint: c (1 second later)
5. delayedPrint: c (directly after) 5. end of loop (right after)

你是否清楚它们行为不同的区别以及缘由呢?

答案基于这样一个事实,method1 使用 forEach() 函数来遍历数组。每次迭代时,它都会调用一个被标记为 async(所以是一个 Future)的新回调函数。执行该回调直到遇到 await,然后将剩余的代码推送到 Event 队列。一旦迭代完成,它就会执行下一个语句:“print(‘end of loop’)”。执行完成后,事件循环 将处理已注册的 3 个回调。

对于 method2,全部的内容都运行在一个相同的代码「块」中,所以可以一行一行按照顺序执行(在本例中)。

正如你所看到的,即便在看起来很是简单的代码中,咱们仍然须要牢记事件循环的工做方式……


多线程

所以,咱们在 Flutter 中如何并行运行代码呢?这可能吗?

是的,这多亏了 Isolates


Isolate 是什么?

正如前面解释过的, IsolateDart 中的 线程

然而,它与常规「线程」的实现存在较大差别,这也是将其命名为「Isolate」的缘由。

「Isolate」在 Flutter 中并不共享内存。不一样「Isolate」之间经过「消息」进行通讯。


每一个 Isolate 都有本身的事件循环

每一个「Isolate」都拥有本身的「事件循环」及队列(MicroTask 和 Event)。这意味着在一个 Isolate 中运行的代码与另一个 Isolate 不存在任何关联。

多亏了这一点,咱们能够得到并行处理的能力。


如何启动 Isolate?

根据你运行 Isolate 的场景,你可能须要考虑不一样的方法。

1. 底层解决方案

第一个解决方案不依赖任何软件包,它彻底依赖 Dart 提供的底层 API。

1.1. 第一步:建立并握手

如前所述,Isolate 不共享任何内存并经过消息进行交互,所以,咱们须要找到一种方法在「调用者」与新的 isolate 之间创建通讯。

每一个 Isolate 都暴露了一个将消息传递给 Isolate 的被称为「SendPort」的端口。(我的以为该名字有一些误导,由于它是一个接收/监听的端口,但这毕竟是官方名称)。

这意味着「调用者」和「新的 isolate」须要互相知道彼此的端口才能进行通讯。这个握手的过程以下所示:

//
// 新的 isolate 端口
// 该端口将在将来使用
// 用来给 isolate 发送消息
//
SendPort newIsolateSendPort;

//
// 新 Isolate 实例
//
Isolate newIsolate;

//
// 启动一个新的 isolate
// 而后开始第一次握手
//
//
void callerCreateIsolate() async {
    //
    // 本地临时 ReceivePort
    // 用于检索新的 isolate 的 SendPort
    //
    ReceivePort receivePort = ReceivePort();

    //
    // 初始化新的 isolate
    //
    newIsolate = await Isolate.spawn(
        callbackFunction,
        receivePort.sendPort,
    );

    //
    // 检索要用于进一步通讯的端口
    //
    //
    newIsolateSendPort = await receivePort.first;
}

//
// 新 isolate 的入口
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 一个 SendPort 实例,用来接收来自调用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向调用者提供此 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 进一步流程
    //
}
复制代码

约束 isolate 的「入口必须是顶级函数或静态方法。

1.2. 第二步:向 Isolate 提交消息

如今咱们有了向 Isolate 发送消息的端口,让咱们看看如何作到这一点:

//
// 向新 isolate 发送消息并接收回复的方法
//
//
// 在该例中,我将使用字符串进行通讯操做
// (发送和接收的数据)
//
Future<String> sendReceive(String messageToBeSent) async {
    //
    // 建立一个临时端口来接收回复
    //
    ReceivePort port = ReceivePort();

    //
    // 发送消息到 Isolate,而且
    // 通知该 isolate 哪一个端口是用来提供
    // 回复的
    //
    newIsolateSendPort.send(
        CrossIsolatesMessage<String>(
            sender: port.sendPort,
            message: messageToBeSent,
        )
    );

    //
    // 等待回复并返回
    //
    return port.first;
}

//
// 扩展回调函数来处理接输入报文
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 初始化一个 SendPort 来接收来自调用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向调用者提供该 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 监听输入报文、处理并提供回复的
    // Isolate 主程序
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

        //
        // 处理消息
        //
        String newMessage = "complemented string " + incomingMessage.message;

        //
        // 发送处理的结果
        //
        incomingMessage.sender.send(newMessage);
    });
}

//
// 帮助类
//
class CrossIsolatesMessage<T> {
    final SendPort sender;
    final T message;

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
复制代码
1.3. 第三步:销毁这个新的 Isolate 实例

当你再也不须要这个新的 Isolate 实例时,最好经过如下方法释放它:

//
// 释放一个 isolate 的例程
//
void dispose(){
    newIsolate?.kill(priority: Isolate.immediate);
    newIsolate = null;
}
复制代码
1.4. 特别说明 - 单监听器流

你可能已经注意到咱们正在使用在「调用者」和新 isolate 之间进行通讯。这些的类型为:「单监听器」流。


2. 一次性计算

若是你只须要运行一些代码来完成一些特定的工做,而且在工做完成以后不须要与 Isolate 进行交互,那么这里有一个很是方便的称为 computeHelper

主要包含如下功能:

  • 产生一个 Isolate
  • 在该 isolate 上运行一个回调函数,并传递一些数据,
  • 返回回调函数的处理结果,
  • 回调执行后终止 Isolate

约束

「回调」函数必须是顶级函数而且不能是闭包或类中的方法(静态或非静态)。


3. 重要限制

在撰写本文时,发现这点十分重要

Platform-Channel 通讯仅仅主 isolate 支持。该主 isolate 对应于应用启动时建立的 isolate

也就是说,经过编程建立的 isolate 实例,没法实现 Platform-Channel 通讯……

不过,仍是有一个解决方法的……请参考此链接以得到关于此主题的讨论。


我应该何时使用 Futures 和 Isolate?

用户将根据不一样的因素来评估应用的质量,好比:

  • 特性
  • 外观
  • 用户友好性
  • ……

你的应用能够知足以上全部因素,但若是用户在一些处理过程当中遇到了卡顿,这极有可能对你不利。

所以,如下是你在开发过程当中应该系统考虑的一些点:

  1. 若是代码片断不能被中断,使用传统的同步过程(一个或多个相互调用的方法);
  2. 若是代码片断能够独立运行而不影响应用的性能,能够考虑经过 Future 使用事件循环
  3. 若是繁重的处理可能须要一些时间才能完成,而且可能影响应用的性能,考虑使用 Isolate

换句话说,建议尽量地使用 Future(直接或间接地经过 async 方法),由于一旦事件循环拥有空闲时间,这些 Future 的代码就会被执行。这将使用户感受事情正在被并行处理(而咱们如今知道事实并不是如此)。

另一个能够帮助你决定使用 FutureIsolate 的因素是运行某些代码所须要的平均时间。

  • 若是一个方法须要几毫秒 => Future
  • 若是一个处理流程须要几百毫秒 => Isolate

如下是一些很好的 Isolate 选项:

  • JSON 解码:解码 JSON(HttpRequest 的响应)可能须要一些时间 => 使用 compute
  • 加密:加密可能很是耗时 => Isolate
  • 图像处理:处理图像(好比:剪裁)确实须要一些时间来完成 => Isolate
  • 从 Web 加载图像:该场景下,为何不将它委托给一个彻底加载后返回完整图像的 Isolate

结论

我认为了解事件循环的工做原理很是重要。

一样重要的是要谨记 FlutterDart)是单线程的,所以,为了取悦用户,开发者必须确保应用运行尽量流畅。FutureIsolate 是很是强大的工具,它们能够帮助你实现这一目标。

请继续关注新文章,同时……祝你编程愉快!

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏