我们知道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之间可以直接异步地发送和处理消息。
2.2 Actor并发通信消息模型
其实在Actor更具体并发消息模型中,还隐藏着另一个概念那就是 mailbox(信箱) 的概念,也就是说Actor之间不能直接发送和接收消息通信的, 而是发送到一个 信箱(mailbox)。其实一般在Actor实现中,一个Actor内部都会有一个对应的mailbox(信箱)实现,用于对消息发送和接收处理。
- 比如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。
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).
//实现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]);
}
复制代码
输出结果:
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带上, 最后才能实现向对方发送消息实现互相通信。继续接着用上面已经成功建立连接例子分析:
//实现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!');
}
复制代码
运行结果:
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、从网络中加载大图
作者:熊喵先生
链接:https://juejin.cn/post/6936899202987327518
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。