Flutter异步编程-Isolate

1,352 阅读13分钟

我们知道Dart是单线程模型,也就是实现异步需要借助EventLoop来进行事件驱动。所以Dart只有一个主线程,其实在Dart中并不是叫 Thread ,而是有个专门名词叫 isolate(隔离)。其实在Dart也会遇到一些耗时计算的任务,不建议把任务放在主isolate中,否则容易造成UI卡顿,需要开辟一个单独isolate来独立执行耗时任务,然后通过消息机制把最终计算结果发送给主isolate实现UI的更新。 在Dart中异步是并发方案的基础,Dart支持单个和多个isolate中的异步。

1. 为什么需要isolate

在Dart/Flutter应用程序启动时,会启动一个主线程其实也就是Root Isolate, 在Root Isolate内部运行一个EventLoop事件循环。所以所有的Dart代码都是运行在Isolate之中的,它就像是机器上的一个小空间,具有自己的私有内存块和一个运行事件循环的单个线程。isolate是提供了Dart/Flutter程序运行环境,包括所需的内存以及事件循环EventLoop对事件队列和微任务队列的处理。来张图理解下Root Isolate在Flutter应用程序中所处作用:

2. 什么是isolate

用官方文档中定义一句话来概括: An isolated Dart execution context .大概的意思就是isolate实际就是一个隔离的Dart执行的上下文环境(或者容器)。isolate是Dart对Actor并发模型的实现。运行中的Dart程序由一个或多个Actor组成,这些Actor其实也就是Dart中的isolate。isolate是有自己的内存和单线程控制的事件循环。isolate本身的意思是“隔离”,因为isolate之间的内存在逻辑上是隔离的,不像Java一样是共享内存的。isolate中的代码是按顺序执行的,任何Dart程序的并发都是运行多个isolate的结果。因为Dart没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不存在死锁的问题。

)

2.1 什么是Actor并发模型

Actor类似于面向对象(OOP)编程中的对象——其封装了状态,并通过消息与其他Actor通信。 在面向对象中,我们使用方法调用的方式去传递信息,而在Actor中,则使用发送消息去传递信息。一个object接收消息(对应为OOP中的方法调用),然后基于该消息来做一些事情。但是Actor并发模型的不同之处在于:每个Actor是完全隔离(和isolate是一致的),他们不会共享内存;同时,Actor也会维护自身的私有状态,并且不会直接被其他的Actor修改。每个Actor之间都彼此隔离所以它们是通过发消息来进行通信的, 在Actor模型中每个工作者被称为actor。Actor之间可以直接异步地发送和处理消息image.png

2.2 Actor并发通信消息模型

其实在Actor更具体并发消息模型中,还隐藏着另一个概念那就是 mailbox(信箱) 的概念,也就是说Actor之间不能直接发送和接收消息通信的, 而是发送到一个 信箱(mailbox)。其实一般在Actor实现中,一个Actor内部都会有一个对应的mailbox(信箱)实现,用于对消息发送和接收处理。

image.png

  • 比如ActorA想和ActorB通信,必须通过发消息方式给ActorB发送一个“邮件(message)”,地址就填ActorB的mailbox地址,至于ActorB是否接收这份“邮件(message)”交由ActorB自己决定。
  • 每个Actor都有一个自己的mailbox, 任意的Actor都可以对应的Actor的mailbox地址发送“邮件(message)”,“邮件(message)”投递和读取是两个过程这样一来就把Actor之间交互通信给完全解耦了.

2.2 Actor并发模型的特征

  • 在Actor内部可以进行计算,且不会占用调用方CPU的时间片, 甚至包括并发策略都是自己决定的
  • 在Actor之间可以通过发送异步消息进行通信
  • 在Actor内部可以保存状态以及修改状态

2.3 Actor并发模型的规则

  • 所有的计算都是在Actor中执行的
  • Actor之间只能通过消息进行通信
  • 当一个Actor接收到消息,可以做3个操作: 发送消息给其他的Actor、创建其他的Actor、接受并处理消息,修改自己的状态

2.4 Actor并发模型的优点

  • Actor可以独立更新和升级,因为每个Actor都是相对独立的个体,彼此独立,互不影响
  • Actor可以支持本地调用和远程调用,因为Actor本质上还是使用基于消息的通讯机制,无论是和本地的Actor,还是远程Actor交互,都是通过消息通信
  • Actor拥有很好错误处理机制,不需要当心消息超时或者等待的问题,因为Actor之间消息都是基于异步方式发送的
  • Actor高效、扩展性也很强,因为支持本地调用和远程调用,当本地Actor处理能力有限可以使用远程Actor来协同处理

2.5 isolate并发模型特点

isolate可以理解为是概念上Thread线程,但是它和Thread线程唯一不一样的就是多个isolate之间彼此隔离且不共享内存空间,每个isolate都有自己独立内存空间,从而避免了锁竞争。由于每个isolate都是隔离,它们之间的通信就是基于上面Actor并发模型中发送异步消息来实现通信的,所以更直观理解把一个isolate当作Actor并发模型中一个Actor即可。在isolate中还有Port的概念,分为send port和receive port可以把它理解为Actor模型中每个Actor内部都有对mailbox(信箱)的实现,可以很好地管理Message。 image.png

3. 如何使用isolate

3.1 isolate包介绍

使用isolate类进行并发操作,需要导入 isolate 

import 'dart:isolate';

该Library主要包含下面:

  • Isolate 类: Dart代码执行的隔离的上下文环境
  • ReceivePort 类: 它是一个接收消息的 Stream , ReceivePort 可以生成 SendPort ,可以由上面的Actor模型规则就知道 ReceivePort 接收消息,可以把消息发送给其他的 isolate 所以要发送消息就需要生成 SendPort , 然后再由 SendPort 发送给对应isolate的 ReceivePort .
  • SendPort 类: 将消息发送给isolate, 准确的来说是将消息发送到isolate中的 ReceivePort 

此外可以使用 spawn 方法生成一个新的 isolate 对象, spawn 是一个静态方法返回的是一个 Future<Isolate> , 必传参数有两个,函数 entryPoint 和参数 message ,其中 **entryPoint函数必须是顶层函数(这个概念之前文章有提到过)或静态方法,参数message需要包含SendPort. **

  external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused: false,
      bool errorsAreFatal,
      SendPort onExit,
      SendPort onError,
      @Since("2.3") String debugName});

3.2 创建和启动isolate

有时候需要根据特定场景采用不同方式创建和启动isolate.

创建一个新的Isolate以及建立RootIsolate和NewIsolate握手连接

正如前面所描述的, isolate 不会共享任何的内存空间且它们之间的通信是通过异步消息实现的。因此需要找到一种在 rootIsolate 和新创建的 newIsolate 之间建立的通信方式。 每个 isolate 都会向外暴露一个端口(Port), 用于向该 isolate 发送消息的端口称为 SendPort . 这就意味要实现 rootIsolate 和新创建的 newIsolate 通信,必须知道彼此的端口(Port).  image.png

//实现newIsolate与rootIsolate(默认)建立连接
import 'dart:isolate';

//定义一个newIsolate
late Isolate newIsolate;
//定义一个newIsolateSendPort, 该newIsolateSendPort需要让rootIsolate持有,
//这样在rootIsolate中就能利用newIsolateSendPort向newIsolate发送消息
late SendPort newIsolateSendPort;

void main() {
  establishConn(); //建立连接
}

//特别需要注意:establishConn执行环境是rootIsolate
void establishConn() async {
  //第1步: 默认执行环境下是rootIsolate,所以创建的是一个rootIsolateReceivePort
  ReceivePort rootIsolateReceivePort = ReceivePort();
  //第2步: 获取rootIsolateSendPort
  SendPort rootIsolateSendPort = rootIsolateReceivePort.sendPort;
  //第3步: 创建一个newIsolate实例,并把rootIsolateSendPort作为参数传入到newIsolate中,为的是让newIsolate中持有rootIsolateSendPort, 这样在newIsolate中就能向rootIsolate发送消息了
  newIsolate = await Isolate.spawn(createNewIsolateContext, rootIsolateSendPort); //注意createNewIsolateContext这个函数执行环境就会变为newIsolate, rootIsolateSendPort就是createNewIsolateContext回调函数的参数
  //第7步: 通过rootIsolateReceivePort接收到来自newIsolate的消息,所以可以注意到这里是await 因为是异步消息
  //只不过这个接收到的消息是newIsolateSendPort, 最后赋值给全局newIsolateSendPort,这样rootIsolate就持有newIsolate的SendPort
  var messageList = await rootIsolateReceivePort.first;
  //第8步,建立连接成功
  print(messageList[0] as String);
  newIsolateSendPort = messageList[1] as SendPort;
}

//特别需要注意:createNewIsolateContext执行环境是newIsolate
void createNewIsolateContext(SendPort rootIsolateSendPort) async {
  //第4步: 注意callback这个函数执行环境就会变为newIsolate, 所以创建的是一个newIsolateReceivePort
  ReceivePort newIsolateReceivePort = ReceivePort();
  //第5步: 获取newIsolateSendPort, 有人可能疑问这里为啥不是直接让全局newIsolateSendPort赋值,注意这里执行环境不是rootIsolate
  SendPort newIsolateSendPort = newIsolateReceivePort.sendPort;
  //第6步: 特别需要注意这里,这里是利用rootIsolateSendPort向rootIsolate发送消息,只不过发送消息是newIsolate的SendPort, 这样rootIsolate就能拿到newIsolate的SendPort
  rootIsolateSendPort.send(['connect success from new isolate', newIsolateSendPort]);
}

输出结果:

image.png

3.2 isolate另一个封装方法compute

compute方法是一个Flutter SDK中已经封装好的isolate的实现,注意: 只有Flutter SDK中才有定义,在Dart SDK是没有此方法定义的。由于dart中的Isolate比较重量级,UI线程和Isolate中的数据的传输比较复杂,因此flutter为了简化用户代码,在foundation库中封装了一个轻量级compute操作。该方法一般用于只需要运行一段耗时代码,然后完成以后不需要再和isolate有交互,可以直接使用这个封装好的方法。源码位于flutter/foundation/_isolates_io.dart.

Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String debugLabel }) async {
  if (!kReleaseMode) {
    debugLabel ??= callback.toString();
  }
  final Flow flow = Flow.begin();
  Timeline.startSync('$debugLabel: start', flow: flow);
  final ReceivePort resultPort = ReceivePort();
  final ReceivePort errorPort = ReceivePort();
  Timeline.finishSync();
  final Isolate isolate = await Isolate.spawn<_IsolateConfiguration<Q, FutureOr<R>>>(
    _spawn,
    _IsolateConfiguration<Q, FutureOr<R>>(
      callback,
      message,
      resultPort.sendPort,
      debugLabel,
      flow.id,
    ),
    errorsAreFatal: true,
    onExit: resultPort.sendPort,
    onError: errorPort.sendPort,
  );
  final Completer<R> result = Completer<R>();
  errorPort.listen((dynamic errorData) {
    assert(errorData is List<dynamic>);
    assert(errorData.length == 2);
    final Exception exception = Exception(errorData[0]);
    final StackTrace stack = StackTrace.fromString(errorData[1] as String);
    if (result.isCompleted) {
      Zone.current.handleUncaughtError(exception, stack);
    } else {
      result.completeError(exception, stack);
    }
  });
  resultPort.listen((dynamic resultData) {
    assert(resultData == null || resultData is R);
    if (!result.isCompleted)
      result.complete(resultData as R);
  });
  await result.future;
  Timeline.startSync('$debugLabel: end', flow: Flow.end(flow.id));
  resultPort.close();
  errorPort.close();
  isolate.kill();
  Timeline.finishSync();
  return result.future;
}

3.3 isolate存在的限制

需要注意的是: Platform-Channel的通信仅仅在主Isolate中支持,这个主Isolate是在Application被启动时候创建的。换句话说Platform-Channel的通信不能在我们自定义创建的Isolate中运行。

4. isolate之间互相通信

两个isolate需要通信首先要在各自作用域内拿到对方的 SendPort 这样就完成第一步双方握手过程,然后就是注意发送消息时需要再次把当前sendPort带上, 最后才能实现向对方发送消息实现互相通信。继续接着用上面已经成功建立连接例子分析: image.png

//实现newIsolate与rootIsolate互相通信

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

//定义一个newIsolate
late Isolate newIsolate;
//定义一个newIsolateSendPort, 该newIsolateSendPort需要让rootIsolate持有,
//这样在rootIsolate中就能利用newIsolateSendPort向newIsolate发送消息
late SendPort newIsolateSendPort;

void main() {
  establishConn(); //建立连接
}

//特别需要注意:establishConn执行环境是rootIsolate
void establishConn() async {
  //第1步: 默认执行环境下是rootIsolate,所以创建的是一个rootIsolateReceivePort
  ReceivePort rootIsolateReceivePort = ReceivePort();
  //第2步: 获取rootIsolateSendPort
  SendPort rootIsolateSendPort = rootIsolateReceivePort.sendPort;
  //第3步: 创建一个newIsolate实例,并把rootIsolateSendPort作为参数传入到newIsolate中,为的是让newIsolate中持有rootIsolateSendPort, 这样在newIsolate中就能向rootIsolate发送消息了
  newIsolate = await Isolate.spawn(createNewIsolateContext, rootIsolateSendPort); //注意createNewIsolateContext这个函数执行环境就会变为newIsolate, rootIsolateSendPort就是createNewIsolateContext回调函数的参数
  //第7步: 通过rootIsolateReceivePort接收到来自newIsolate的消息,所以可以注意到这里是await 因为是异步消息
  //只不过这个接收到的消息是newIsolateSendPort, 最后赋值给全局newIsolateSendPort,这样rootIsolate就持有newIsolate的SendPort
  newIsolateSendPort = await rootIsolateReceivePort.first;
  //第8步: 建立连接后,在rootIsolate环境下就能向newIsolate发送消息了
  sendMessageToNewIsolate(newIsolateSendPort);
}

//特别需要注意:sendMessageToNewIsolate执行环境是rootIsolate
void sendMessageToNewIsolate(SendPort newIsolateSendPort) async {
  ReceivePort rootIsolateReceivePort = ReceivePort(); //创建专门应答消息rootIsolateReceivePort
  SendPort rootIsolateSendPort = rootIsolateReceivePort.sendPort;
  newIsolateSendPort.send(['this is from root isolate: hello new isolate!', rootIsolateSendPort]);//注意: 为了能接收到newIsolate回复消息需要带上rootIsolateSendPort
  //第11步: 监听来自newIsolate的消息
  print(await rootIsolateReceivePort.first);
}

//特别需要注意:createNewIsolateContext执行环境是newIsolate
void createNewIsolateContext(SendPort rootIsolateSendPort) async {
  //第4步: 注意callback这个函数执行环境就会变为newIsolate, 所以创建的是一个newIsolateReceivePort
  ReceivePort newIsolateReceivePort = ReceivePort();
  //第5步: 获取newIsolateSendPort, 有人可能疑问这里为啥不是直接让全局newIsolateSendPort赋值,注意这里执行环境不是rootIsolate
  SendPort newIsolateSendPort = newIsolateReceivePort.sendPort;
  //第6步: 特别需要注意这里,这里是利用rootIsolateSendPort向rootIsolate发送消息,只不过发送消息是newIsolate的SendPort, 这样rootIsolate就能拿到newIsolate的SendPort
  rootIsolateSendPort.send(newIsolateSendPort);
  //第9步: newIsolateReceivePort监听接收来自rootIsolate的消息
  receiveMsgFromRootIsolate(newIsolateReceivePort);
}

//特别需要注意:receiveMsgFromRootIsolate执行环境是newIsolate
void receiveMsgFromRootIsolate(ReceivePort newIsolateReceivePort) async {
  var messageList = (await newIsolateReceivePort.first) as List;
  print('${messageList[0] as String}');
  final messageSendPort = messageList[1] as SendPort;
  //第10步: 收到消息后,立即向rootIsolate 发送一个回复消息
  messageSendPort.send('this is reply from new isolate: hello root isolate!');
}

运行结果:

image.png

5. isolate和普通Thread的区别

isolate和普通Thread的区别需要从不同的维度来区分:

  • 1、从底层操作系统维度

在isolate和Thread操作系统层面是一样的,都是会去创建一个OSThread,也就是说最终都是委托创建操作系统层面的线程。

  • 2、从所起的作用维度

都是为了应用程序提供一个运行时环境。

  • 3、从实现机制的维度

isolate和Thread有着明显区别就是大部分情况下的Thread都是共享内存的,存在资源竞争等问题,但是isolate彼此之间是不共享内存的。

6. 什么场景该使用Future还是isolate

其实这个问题,更值得去注意,因为这是和实际的开发直接相关,有时候确实需要知道什么时候应该是 Future ,什么时候应该使用 isolate . 有的人说使用 isolate 比较重,一般不建议采用,其实不能这样一概而论。 isolate 也是有使用场景的,可能有的小伙伴会疑惑网络请求应该算耗时吧,平时一般使用 Future 就够了,为什么不使用 isolate 呢。这里将一一给出答案。

  • 如果一段代码不会被中断,那么就直接使用正常的同步执行就行。
  • 如果代码段可以独立运行而不会影响应用程序的流畅性,建议使用 Future 
  • 如果繁重的处理可能要花一些时间才能完成,而且会影响应用程序的流畅性,建议使用 isolate 

换句话说,建议尽可能多地使用 Future (直接或间接通过异步方法),因为一旦 EventLoop 有空闲期,这些 Future 的代码就会运行。这么说可能还是有点抽象,下面给出一个代码运行时间指标衡量选择:

  • 如果一个方法要花费几毫秒时间那么建议使用 Future .
  • 如果一个处理可能需要几百毫秒那么建议使用 isolate .

下面列出一些使用 isolate 的具体场景:

  • 1、JSON解析: 解码JSON,这是HttpRequest的结果,可能需要一些时间,可以使用封装好的 isolate 的 compute 顶层方法。
  • 2、加解密: 加解密过程比较耗时
  • 3、图片处理: 比如裁剪图片比较耗时
  • 4、从网络中加载大图

7. 熊喵先生的小总结

到这里有关Dart异步编程中的isolate相关内容就介绍完毕,这篇文章对isolate分析得还是很全面的,包括从为什么需要isolate到怎么使用isolate到isolate的使用场景到最后的root isolate的启动源码分析。

感谢关注,熊喵先生愿和你在技术路上一起成长!