本项目基于macOS桌面程序,源码地址:https://github.com/NeverOvO/learningfoundation
参考文章:https://www.jianshu.com/p/a524620e4bb5
https://juejin.cn/post/6844904148303872013
https://cloud.tencent.com/developer/article/1676442
- NeverOuO
Isolate基础解释
Dart/Flutter中最常用的异步执行使用的是async 和 Future,通常在网络请求,处理耗时任务且需要结果来进行下一步时比较常用。
如果遇到大数据量的计算,那么使用async 和 Future可能会使UI出现明显的卡顿与OOM,所以也需要进行并行操作,即Isolate。
官方的定义介绍:
isolate是Dart对actor并发模式的实现。运行中的Dart程序由一个或多个actor组成,这些actor也就是Dart概念里面的isolate。isolate是有自己的内存和单线程控制的运行实体。isolate本身的意思是“隔离”,因为isolate之间的内存在逻辑上是隔离的。isolate中的代码是按顺序执行的,任何Dart程序的并发都是运行多个isolate的结果。因为Dart没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不用担心死锁的问题。
由于isolate之间没有共享内存,所以他们之间的通信唯一方式只能是通过Port进行,而且Dart中的消息传递总是异步的。
我们可以看到isolate神似Thread,但实际上两者有本质的区别。操作系统内的线程之间是可以有共享内存的而isolate没有,这是最为关键的区别。
原文链接:https://blog.csdn.net/u011578734/article/details/108853613
学习使用Isolate的初衷,是在于我想完成一个讲图片分割提取主要颜色,进而转换为像素画的一个项目Demo,而在测试中,当按图片10宽度来切割一个1920X1080图片时,APP发生明显卡顿,虽然最后发现内存全部使用在了渲染之上,分割代码仅需要极少资源。
Isolate的基本使用
1:起因
依旧是采用引用文章里的计算1+2+...100000000000的和来梳理。
int sum(int num) {
int count = 0;
while (num > 0) {
count = count + num;
num--;
}
return count;
}
int result = sum(10000000000);
执行上诉代码,卡顿非常明显,UI直接无响应。使用async也是无济于事。那么就到了isolate的主场。
使用 isolate
开辟新线程,避开主线程,不干扰UI刷新,计算时间与消耗虽然相同,但是不会影响用户的使用体验,加上加载等待提示,会有一个比较好的效果。
2:教程中使用的代码
_testIsolate() async {
ReceivePort rp1 = new ReceivePort();
SendPort port1 = rp1.sendPort;
// 通过spawn新建一个isolate,并绑定静态方法
Isolate? newIsolate = await Isolate.spawn(doWork, port1);
SendPort? port2;
rp1.listen((message) {
print("rp1 收到消息: $message"); //2. 4. 7.rp1收到消息
if(message == "完成"){
newIsolate!.kill(priority: Isolate.immediate);
newIsolate = null;
print("杀掉");
}
if (message[0] == 0) {
port2 = message[1]; //得到rp2的发送器port2
} else {
if (port2 != null) {
print("port2 发送消息");
port2?.send([1, "这条信息是 port2 在main isolate中 发送的"]); // 8.port2发送消息
}
}
});
print("port1--main isolate发送消息");
port1.send([1, "这条信息是 port1 在main isolate中 发送的"]); //1.port1发送消息
// newIsolate.kill();
}
// 新的isolate中可以处理耗时任务
static void doWork(SendPort port1) {
ReceivePort rp2 = new ReceivePort();
SendPort port2 = rp2.sendPort;
rp2.listen((message) {
//9.10 rp2收到消息
print("rp2 收到消息: $message");
});
// 将新isolate中创建的SendPort发送到main isolate中用于通信
print("port1--new isolate发送消息");
port1.send([0, port2]); //3.port1发送消息,传递[0,rp2的发送器]
// 模拟耗时5秒
sleep(Duration(seconds: 1));
print("port1--new isolate发送消息");
port1.send([1, "这条信息是 port1 在new isolate中 发送的"]); //5.port1发送消息
print("port2--new isolate发送消息");
port2.send([1, "这条信息是 port2 在new isolate中 发送的"]); //6.port2发送消息
}
I/flutter (14639): port1--main isolate发送消息
I/flutter (14639): rp1 收到消息: [1, 这条信息是 port1 在main isolate中 发送的]
I/flutter (14639): port1--new isolate发送消息
I/flutter (14639): rp1 收到消息: [0, SendPort]
I/flutter (14639): port1--new isolate发送消息
I/flutter (14639): port2--new isolate发送消息
I/flutter (14639): rp1 收到消息: [1, 这条信息是 port1 在new isolate中 发送的]
I/flutter (14639): port2 发送消息
I/flutter (14639): rp2 收到消息: [1, 这条信息是 port2 在new isolate中 发送的]
I/flutter (14639): rp2 收到消息: [1, 这条信息是 port2 在main isolate中 发送的]
上述代码基本阐述了Ioslate的运行逻辑,与2个线程直接的沟通方式,因为2个isolate之间的内存是相互隔离不共享的,因此也不存在锁竞争问题,两个Isolate完全是两条独立的执行线,且每个Isolate都有自己的事件循环,它们之间只能通过发送消息通信,所以它的资源开销低于线程。
在dowork中
port1.send([0, port2]);
将SendPort? port2传递给主线程,是构成2个线程沟通的核心
这里的message可以是任何数据类型。
3:常规方案
我拿到上述方案后,对计算1+2+...100000000000的和进行改造,代码如下:
//常规方案 需要手动进行关闭线程
_testIsolate() async {
ReceivePort rp1 = new ReceivePort();
SendPort port1 = rp1.sendPort;
// 通过spawn新建一个isolate,并绑定静态方法
// Isolate? newIsolate = await Isolate.spawn(doWork, port1);
Isolate? newIsolate = await Isolate.spawn(doWork1, port1);
SendPort? port2;
rp1.listen((message) {
print("rp1 收到消息: $message"); //2. 4. 7.rp1收到消息
if(message[0] == -1){
newIsolate!.kill(priority: Isolate.immediate);
newIsolate = null;
print("杀掉线程");
}
if (message[0] == 0) { // 对接完成后可以进行一次操作
port2 = message[1]; //对接
port2!.send([0,"对接完成"]);
port2!.send([1,100000000]);
}
if(message[0] == 1){ // 这里用来输出结果,完成这一次的操作
content = "总和${message[1]}";
setState(() {
});
}
});
}
// 新的isolate中可以处理耗时任务
static void doWork1(SendPort port1) {
ReceivePort rp2 = new ReceivePort();
SendPort port2 = rp2.sendPort;
port1.send([0, port2]);
rp2.listen((message) {
//9.10 rp2收到消息
print("rp2 收到消息: $message");
if(message[0] == 1){ // 对接完成后进行操作
num result = summ(message[1]);
port1.send([1,result]);
sleep(Duration(seconds: 1));
port1.send([-1]);
}
});
// 模拟耗时5秒
// sleep(Duration(seconds: 2));
// port1.send([1,"任务完成"]);
}
这里的message[0] == -1,message[0] == 0,是我自己拟定的用来区分沟通功能的代码,这套代码的逻辑是:首先生成newIsolate新线程执行doWork1,在doWork1生成后讲port1.send([0, port2])发回给主线程,形成2个线程之间的沟通渠道。
if (message[0] == 0) { // 对接完成后可以进行一次操作
port2 = message[1]; //对接
port2!.send([0,"对接完成"]);
port2!.send([1,100000000]);
}
完成对接之后,讲需要计算的参数100000000发给dowork1进行计算
if(message[0] == 1){ // 对接完成后进行操作
num result = summ(message[1]);
port1.send([1,result]);
sleep(Duration(seconds: 1));
port1.send([-1]);
}
计算完成后讲结果发回给主线程,休眠1秒后,发送关闭这个线程的指令来释放内存与线程。
4:改进方案
isolate 新特性
Dart 2.15 更新, 给 iso 添加了组的概念,isolate 组 工作特征可简单总结为以下两点:
Isolate 组中的 isolate 共享各种内部数据结构
Isolate 组仍然阻止在 isolate 间共享访问可变对象,但由于 isolate 组使用共享堆实现,这也让其拥有了更多的功能。
工作器 isolate 通过网络调用获得数据,将该数据解析为大型 JSON 对象图,然后将这个 JSON 图返回到主 isolate 中。
Dart 2.15 之前:执行该操作需要深度复制,如果复制花费的时间超过帧预算时间,就会导致界面卡顿。
使用 Dart 2.15:工作器 isolate 可以调用 Isolate.exit(),将其结果作为参数传递。然后,Dart 运行时将包含结果的内存数据从工作器 isolate 传递到主 isolate 中,无需复制,且主 isolate 可以在固定时间内接收结果。
故将下述的2处代码直接替换为
Isolate.exit(port1, [1,summ(message[1])]);
port1.send([-1]);
if(message[0] == -1){
newIsolate!.kill(priority: Isolate.immediate);
newIsolate = null;
print("杀掉线程");
}
这里能够实现在传递结果的同时关闭线程,同时Isolate.exit的方法会比send方法更加节省资源:
隔离之间的消息传递通常涉及数据复制,因此可能会很慢,并且会随着消息大小的增加而增加。但是
链接:https://www.jianshu.com/p/a524620e4bb5exit()
,则是在退出隔离中保存消息的内存,不会被复制,而是被传输到主 isolate。这种传输很快,并且在恒定的时间内完成。
故,改进后的代码为:
//改进方案 任务完成后自动关闭线程
_testIsolate1() async {
ReceivePort rp1 = new ReceivePort();
SendPort port1 = rp1.sendPort;
// 通过spawn新建一个isolate,并绑定静态方法
// Isolate? newIsolate = await Isolate.spawn(doWork, port1);
Isolate? newIsolate = await Isolate.spawn(doWork2, port1);
SendPort? port2;
rp1.listen((message) async {
print("rp1 收到消息: $message"); //2. 4. 7.rp1收到消息
if (message[0] == 0) { // 对接完成后可以进行一次操作
port2 = message[1]; //对接
port2!.send([0,"对接完成"]);
port2!.send([1,100000000]);
}
if(message[0] == 1){ // 这里用来输出结果,完成这一次的操作
content = "总和${message[1]}";
setState(() {
});
}
});
// port2!.send(100); //1.port1发送消息
// newIsolate.kill();
}
// 新的isolate中可以处理耗时任务
static void doWork2(SendPort port1) {
ReceivePort rp2 = new ReceivePort();
SendPort port2 = rp2.sendPort;
port1.send([0, port2]);
rp2.listen((message) async {
print("rp2 收到消息: $message");
if(message[0] == 1){ // 对接完成后进行操作
Isolate.exit(port1, [1,summ(message[1])]);
}
});
}
这里如果有多次任务 依然可以使用send或多开线程的方案来实现。
后续有新的isolate学习再更新。我的像素画方案依然还没有着落。悲