在前面的一篇文章中大致介绍了如何在Flutter中进行异步编程,参考:

本文介绍其背后的相关原理。

Isolate

一个Isolate就是Dart代码执行的地方,类似于Java中的线程Thread,但是Isolate之间没有共享内存(好处是对于内存分配和垃圾回收时,不需要加锁机制),每个Isolate都有自己的内存和一个事件循环(类似于Android中的Handler/Looper机制)

默认Dart代码都在一个主Isolate中执行,但是如果一个任务计算量很大,已经导致了丢帧,那么可以创建一个新的Isolate来执行该逻辑,通过 Isolate.spawn()或者 Isolate.spawnUri()来创建Isolate,Isolate之间只能通过 SendPort/ReceivePort机制来进行发送和接收数据。

使用Isolate.spawnUri创建Isolate

比如在主Isolate中添加如下代码:

import 'dart:isolate';

void main(List<String> arguments) {
  print("main isolate start");
  createIsolate();
  print("main isolate stop");
}

createIsolate() async{
  ReceivePort rp = new ReceivePort();
  SendPort port = rp.sendPort;
  Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_isolate.dart"), ["hello Isolate", "this is args"], port);
  SendPort sendPort;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      sendPort = message[1];
    }else{
      sendPort?.send([1,"这条信息是main Isolate发送的"]);
    }
  });
}

然后,在主Isolate文件的同级目录下新建一个other_isolate.dart文件,代码如下:

import 'dart:isolate';
import  'dart:io';

void main(args, SendPort sendPort) {
  print("child isolate start");
  print("child isolate args: $args");
  ReceivePort receivePort = new ReceivePort();
  SendPort port = receivePort.sendPort;
  receivePort.listen((message){
    print("child_isolate message: $message");
  });

  sendPort.send([0, port]);
  sleep(Duration(seconds:5));
  sendPort.send([1, "child isolate 任务完成"]);
  print("child isolate stop");
}

运行主Isolate文件代码,最终的输出结果如下:

main isolate start
main isolate stop
child isolate start
child isolate args: [hello Isolate, this is args]
main isolate message: [0, SendPort]
child isolate stop
main isolate message: [1, child isolate 任务完成]
child_isolate message: [1, 这条信息是main Isolate发送的]

使用Isolate.spawn创建Isolate

通常我们会使用spawn方式创建Isolate,我们希望将新创建的Isolate代码和主Isolate代码写在同一个文件,且不希望出现两个main函数,并且将耗时函数运行在新的Isolate,这样做的目的是有利于代码的组织与复用。

比如一个计算阶乘的Isolate于主Isolate之间的通信:

import 'dart:isolate';

Future<void> main(List<String> arguments) async {
  print(await asyncFibonacci(20));     //计算20的阶乘
}

Future<dynamic> asyncFibonacci(int n) async{
  final response = new ReceivePort();
  await Isolate.spawn(isolate,response.sendPort);
  final sendPort = await response.first as SendPort;
  final answer = new ReceivePort();
  sendPort.send([n,answer.sendPort]);
  return answer.first;
}

void isolate(SendPort initialReplyTo){
  final port = new ReceivePort();
  initialReplyTo.send(port.sendPort);
  port.listen((message){
    final data = message[0] as int;
    final send = message[1] as SendPort;
    send.send(syncFibonacci(data));
  });
}

int syncFibonacci(int n){
  return n < 2 ? n : syncFibonacci(n-2) + syncFibonacci(n-1);
}

在上面的代码中,耗时的操作放在使用spawn方法创建的Isolate中。运行上面的程序,最终的输出结果为6765,即20的阶乘。

事件循环

Dart中的Isolate里运行着事件循环机制,流程如下图所示:

请输入图片描述

可以看出,将任务加入到微任务中可以被尽快执行,但也需要注意,当事件循环在处理微任务队列时,事件队列会被卡住,此时应用程序无法处理鼠标单击、I/O消息等事件。

通过Future的链式调用来指定任务顺序

我们可以使用Future的then方法来组织Future,包括产生和取值的整个过程,下面是一个对比:

// BAD because of no explicit dependency between setting and using
// the variable.
future.then(...set an important variable...);
Timer.run(() {...use the important variable...});

上面的代码应该修改成下面的样式:

// BETTER because the dependency is explicit.
future.then(...set an important variable...)
  .then((_) {...use the important variable...});

如果在取值使用的时候,逻辑比较耗时或者可以稍后再进行处理,那么可以在取值的地方再创建一个新的Future,这样可以使得事件循环能够空出时机来处理event事件队列中的其他事件,如:

// MAYBE EVEN BETTER: Explicit dependency plus delayed execution.
future.then(...set an important variable...)
  .then((_) {new Future(() {...use the important variable...})});

选择使用正确的队列

通常我们会使用event队列,在我们使用Future时,新建的一个Future会添加一个事件到event队列的尾部,而通过顶级的 scheduleMicrotask()方法,我们可以添加一个任务到microTask队列中。

  • 在event队列中安排任务(使用new Future() 或者new Future.delayed()),delayed延迟后添加到event队列中的任务并不会立即被处理,这依赖于主Isolate是否空闲或者microTask队列里是否有任务未执行完

关于Future的一些有趣事实:

  1. 传入Future的then方法里的函数,会在Future完成时立即执行(这个函数并不会向队列中添加一个新的任务,它只是被调用)
  2. 如果一个Future在then方法被调用之前已经完成了,那么这时候会向microtask队列里添加一个新的任务,这个任务就是执行传入then方法里的函数
  3. Future.value()Future.sync()构造方法都会在microtask队列中完成
  4. Future.microtask构造函数会调用顶级的scheduleMicrotask方法,使得相应的函数能够先于其他异步事件执行(如Timer事件或者DOM事件),如下代码所示:
main() {
  Timer.run(() { print("executed"); });  // Will never be executed.
  foo() {
    scheduleMicrotask(foo);  // Schedules [foo] in front of other events.
  }
  foo();
}

Isolate越多越好吗?

对于计算密集型的任务,不应该放到任何一个队列中,应该新启动一个Isolate,但是我们应该用多少个Isolate呢?

按照文中所说:

How many isolates should you use? For compute-intensive tasks, you should generally use as many isolates as you expect to have CPUs available. Any additional isolates are just wasted if they’re purely computational. However, if the isolates perform asynchronous calls—to perform I/O, for example—then they won’t spend much time on the CPUs, so having more isolates than CPUs makes sense.

You can also use more isolates than CPUs if that’s a good architecture for your app. For example, you might use a separate isolate for each piece of functionality, or when you need to ensure that data isn’t shared.

我的理解是:如果一个Isolate如果仅仅是进行一个计算密集型的任务,那么这时候使用更多的Isolate是没有意义的(如果是超过里CPU核心数的话,因为超出CPU核心数的Isolate,执行时只能共享CPU的时间片了);但是如果是I/O密集型的任务有多个,那么这时候Isolate就不会在CPU上浪费太多时间,因此超过CPU核心数的Isolate来进行I/O密集型任务,也是有意义的。

异步任务的执行顺序

通过上面的介绍,对于函数内部同时有microtask队列和event队列时,会仍然先执行非异步的代码,然后才会执行队列中的代码,当然microtask队列执行会优先于event队列,如下代码:

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)

但是对于更加复杂的顺序,还是要多做分析,比如像下面这样的代码:

请输入图片描述

假设没有其他外部任务进来的话,那么各个时间点下的队列情况和输出会如下所示:

请输入图片描述

总结

  • 应用主函数main(),以及microtask队列和event队列中的所有对象都执行在主Isolate中
  • microtask执行的优先级高,其任务通常来自于Dart内部,如果一个Future在then方法被调用之前已经完成了,那么then内的函数也会运行在microtask内的一个任务中
  • event队列处理来自Dart(futures、timers、isolate messages等)和系统的所有任务(用户的动作交互、I/O事件、绘制事件等)
  • 通常我们都是使用event队列(即通过使用new Future()或者new Future.delayed()

参考:

如果觉得我的文章对你有用,请随意赞赏