本项目基于macOS桌面程序,源码地址:https://github.com/NeverOvO/learningfoundation

参考文章:https://www.jianshu.com/p/a524620e4bb5

https://juejin.cn/post/6844904148303872013

https://cloud.tencent.com/developer/article/1676442

https://www.jianshu.com/p/a9a54b101870

- 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方法更加节省资源:

隔离之间的消息传递通常涉及数据复制,因此可能会很慢,并且会随着消息大小的增加而增加。但是 exit(),则是在退出隔离中保存消息的内存,不会被复制,而是被传输到主 isolate。这种传输很快,并且在恒定的时间内完成。

链接:https://www.jianshu.com/p/a524620e4bb5

故,改进后的代码为:

  //改进方案 任务完成后自动关闭线程
  _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学习再更新。我的像素画方案依然还没有着落。悲