在前面的一篇文章中大致介绍了如何在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的一些有趣事实:
- 传入Future的then方法里的函数,会在Future完成时立即执行(这个函数并不会向队列中添加一个新的任务,它只是被调用)
- 如果一个Future在then方法被调用之前已经完成了,那么这时候会向microtask队列里添加一个新的任务,这个任务就是执行传入then方法里的函数
Future.value()
和Future.sync()
构造方法都会在microtask队列中完成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呢?
按照文中所说:
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()
)
参考: