dart 07.3 并发多线程

什么是isolate

dart 虽然是一个单线程语言 但是不代表他不支持多线程并发

  • 在dart中线程不叫线程叫做isolate(隔离区)所有的代码都运行在这
    • 类似于线程但不共享内存的独立工作程序,仅通过消息进行通信。
    • 每个isolate 都有一个完整的事件循环机制,每个隔离区都有自己的内存堆,确保每个隔离区的状态都不会被其他隔离区访问。
      • 这意味着在一个 Isolate 中运行的代码与另外一个 Isolate不存在任何关联。
      • 依赖这点所以我们通过Isolate实现并发

名词解释

  • 在下面描述中
    • mainIsolate代表调用Isolate或者叫主Isolate
    • newIsolate 代表**新创建的Isolate**
    • mainReceivePort 代表**调用IsolateReceivePort**
    • newReceivePort 代表**新IsolateReceivePort**
    • mainSendPort 代表**调用IsolateSendPort**
    • newSendPort 代表**新IsolateSendPort**

实现Isolate

mainIsolate中创建newIsolate有两种方法 spawn,spawnUri

创建步骤

  1. 创建newIsolate并与当前调用mainIsolate并通过sendPort建立连接
    • 因为Isolate之间不共享内存且通过消息交互,所以**Isolate之间通讯需要持有对方发送消息的端口,文档里叫**SendPort
      • 对于sendPort文档里这样描述:[SendPort]s are created from [ReceivePort]s.
      • 翻译:发送端口是从接收端口创建的
      • RecivePort文档里这样描述:Together with SendPort, the only means of communication between isolates.
      • 与发送端一起,是一种isolate之间的通讯方式
      • 所以Isolate之间通讯需要持有对方的SendPort
    • 创建newIsolate
    • 获得mainIsolatemainRecivePortmainSendPort
      • 要将mainIsolatemainSendPort发送到newIsolate
      • mainRecivePort 监听 mainSendPort 发送回来的数据
      • 至于如何将mainSendPort发送到newIsolate的请看下面的例子
  2. 发送数据
    • 通过各自的sendPort.send
  3. 接收数据通过
    • 各自的RecivePort
  4. 在合适的机会销毁Isolate

下面看具体的步骤

spawn

  • spawn创建并生成与当前Isolate共享相同代码的Isolate。

  • Future spawn(void entryPoint(T message), T message)

    • 可以看到每一个isolate 需要两个必要参数
      • 入口点函数 entryPoint
        • 文档对其这样描述
        • 参数entryPoint指定要在派生的隔离中调用的初始函数,
        • 入口点函数在新的Isolate中被调用,
      • message只有[message]作为entryPoint的参数。
        • message通常用于传送调用IsolateSendPort对象
    • entryPoint函数就是要在**newIsolate**中运行的函数
    • 因为Isolate相互通讯需要持有对方的sendport
      • 所以我们将**mainIsolatemainSendPort** 作为**message传递到将要在newIsolate** 中执行的入口函数(entryPoint)中,使 newIsolate 持有mainSendPort
      • 入口函数在**newIsolate** 中执行,我们再通过**mainSendPort** 将**NewIsolatenewSendPort** 发送到**mainIsolate**中 ,使 mainIsolate持有newSendPort
      • 这样**mainIsolate持有了newSendPort** ,
        • 这样**mainIsolate通过newSendPort** 可以将**mainIsolate中的消息发送到newIsolate**
      • 这样**newIsolate持有了mainSendPort**
        • 这样**newIsolate通过mainSendPort** 可以将**newIsolate中的消息发送到mainIsolate**
      • 所以是通过入口函数将**主Isolate的SendPort发送到Newisolate**
    • 需要注意 isolate 的「入口函数(entryPoint)」必须是顶级函数或静态方法。
  • 示例代码

  • import 'dart:io';
    import 'dart:isolate';
    main() async {
      print("main start");
      createIsolate();
      print("main end");
    }
    
    Isolate newIsolate;
    
    void createIsolate() async {
      // 两个Isolate要想互相通讯须持有对方的的sendPort
      // 获取mainIsolate的监听器 mainReceivePort
      ReceivePort main_rp = ReceivePort();
      // 获取 mainIsolate 的 SendPort 并作为参数传递给newIsolate
      // 使 newIsolate 持有 mainSendPort,用于通讯
      // 使 newIsolate 可以通过 mainSendPort 将 newIsolate 的发送消息回 mainIsolate
      SendPort main_send = main_rp.sendPort;
      // 创建新的isolate
      newIsolate = await Isolate.spawn(excuter, main_send);
      // 这里需要得到 newIsolate 的 SendPort,
      // 让 mainIsolate 持有 newSendPort,用于通讯
      // 使 mainIsolate 可以通过 newSendPort 将 mainIsolate 的发送消息回 newIsolate
      // 注意 这里 newSendPort 是 newIsolate中的mainSendPort 发送回来的所以要在监听中获取newSendPort
      SendPort new_send;
      //主接收器(mainReceivePort)开始监听newIsolate中的mainSendPort发送回来的消息
      main_rp.listen((message) {
        print("NewIsolat通过main_send发送来一条消息 $message ,到主Isolate");
        if (message[0] == 0) {
          // 获取newSendPort
          new_send = message[1] as SendPort;
        } else {
          new_send?.send("mian_isolate 通过new_send发送了一条消息到NewIsolate");
        }
      });
    }
    
    // 入口函数将在newIsolate中执行
    void excuter(SendPort mainSendPort) {
      // 获取newIsolate的监听器newReceivePort
      ReceivePort new_rp = ReceivePort();
      //newReceivePort开始监听 mainIsolate中的newSendPort发送回来的消息
      new_rp.listen((message) {
        print(message);
        // 接收到第一条main发送过来的函数 就销毁newIsolate
        print("销毁NewIsolate");
        destroyNewIsolate();
      });
      // 获取newIsolate的 SendPort 
      SendPort new_send = new_rp.sendPort;
      //将其发送到 mainIsolate
      // 让 mainIsolate 持有 newSendPort,用于通讯
      // 使 mainIsolate 可以通过 newSendPort 将 mainIsolate 的发送消息回 newIsolate
      mainSendPort.send([0, new_send]);
      // 模拟耗时5秒
      sleep(Duration(seconds: 5));
      mainSendPort.send([1, "excuter 任务完成"]);
      print("NewIsolat 执行结束");
    }
    
    //销毁newIsolate
    destroyNewIsolate() {
      // 任务执行结束销毁newIsolate
      newIsolate?.kill(priority: Isolate.immediate);
      newIsolate = null;
    }
    /* 输出
    newIsolat通过main_send发送来一条消息 [0, SendPort] ,到mainIsolate
    newIsolat通过main_send发送来一条消息 [1, excuter 任务完成] ,到mainIsolate
    NewIsolat 执行结束
    mianIsolate 通过new_send发送了一条消息到newIsolate
    销毁NewIsolate
    */
    
    

spawn Uri

  • 创建并派生一个Isolate,该Isolate使用指定的URI从库中运行代码。

  • Isolate.spawnUri(Uri uri,List args,var message);`

  • spawnUri方法有三个必须的参数,

    • 第一个是Uri,指定一个新Isolate代码文件的路径,
    • 第二个是参数列表,类型是List,
    • 第三个是消息。其实是发送消息的端口SendPort
  • 注意这种方式

    • newIsolate代码在一个单独的文件里

    • newIsolate的的执行函数,必须包是一个main函数,它是newIsolate的入口方法,

    • main函数必须可以用零个一个或者两个参数调用

      • `main()`
        `main(args)`
        `main(args, message)`
        
      • main函数中的args参数列表,正对应spawnUri中的第二个参数。如不需要向newIsolate中传参数,该参数可传空List

      • message则是调用Isolate的SendPort

  • 示例代码

    • mainIsolate

      • import 'dart:isolate';
        
        Isolate newIsolate;
        
        main() async {
          ReceivePort mainReceivePort = ReceivePort();
          SendPort mainSendPort = mainReceivePort.sendPort;
          List list = ["hello, isolate", "this is args"];
          var uri = Uri(path: "./newTaskUri.dart");
          // 创建newIsolate 并建立连接
          newIsolate = await Isolate.spawnUri(uri, list, mainSendPort);
          // 需要获取 newSendPort 用于通讯
          // newSendPort 是 newIsolate中的mainSendPort 发送回来的所以要在监听中获取结果
          SendPort newSendPort;
          mainReceivePort.listen((message) {
            print("newIsolat通过main_send发送来一条消息 $message ,到mainIsolate");
            if ("excuter 任务完成" == message[1]) {
              // 销毁newIsolate
              print("销毁newIsolate");
              destroyNewIsolate();
        
            }
            if (message[0] == 0) {
              // 获取newSendPort
              newSendPort = message[1] as SendPort;
            } else {
              newSendPort?.send("mian_isolate 通过new_send发送了一条消息到newIsolate");
            }
          });
        }
        
        //销毁newIsolate
        destroyNewIsolate() {
          // 任务执行结束销毁newIsolate
          newIsolate?.kill(priority: Isolate.immediate);
          newIsolate = null;
        }
        
        
    • newIsolate文件newTaskUri.dart

      • import 'dart:io';
        import 'dart:isolate';
        
        // 这里的main 就是入口函数 在newIsolate中执行
        // 就相当与 spawn中的 excuter
        // 内部执行回传sendport,消息监听发送,逻辑是一样的
        // 区别就是多了一个参数列表可以传一些参数处理些逻辑 功能更丰富了
        void main(args, SendPort mainSendPort) {
          try {
            print("newIsolate 开始");
            print("newIsolate (参数列表)args: $args");
        
            ReceivePort newRecivePort = new ReceivePort();
            //newReceivePort开始监听 newSendPort发送回来的消息
            newRecivePort.listen((message) {
              print(message);
              // 接收到第一条消息
            });
            // 获取newSendPort 并通过mainSendPort 回传到mainIsolate
            SendPort newSendPort = newRecivePort.sendPort;
            mainSendPort.send([0, newSendPort]);
            // 模拟耗时5秒
            sleep(Duration(seconds: 5));
            mainSendPort.send([1, "excuter 任务完成"]);
            print("NewIsolat 执行结束");
          } catch (e) {
            print("myerr $e");
          }
        }
        
    • 输出结果

      • /**
        newIsolate 开始
        newIsolate (参数列表)args: [hello, isolate, this is args]
        newIsolat通过main_send发送来一条消息 [0, SendPort] ,到mainIsolate
        NewIsolat 执行结束
        newIsolat通过main_send发送来一条消息 [1, excuter 任务完成] ,到mainIsolate
        销毁newIsolate
        */
        
      • spawn输出结果对比少了一个mainIsolatnewIsolate发送消息,是因为代码中销毁newIsolate时机不同

        • spawnnewIsolate执行结束后mainIsolatenewIsolate发送消息且被处理后销毁newIsolate
        • spawnUrinewIsolate执行结束后就销毁了newIsolate

需要注意

  1. 无论是上面的**spawn还是spawnUri**,运行后都会创建两个Isolate

    • spawn创建并生成与当前Isolate共享相同代码的新Isolate。
    • spawnUri 一个独立的不与调用Isolate共享代码的新Isolate
  2. 想要相互通讯就必须持有对方的SendPort

    • newIsoalte 要持有mainSendPort 靠创建Isolate是入口点函数传参 参数即`mainSendPort``
    • ``mainIsolate 要持有newSendPort靠传入newIsolatemainSendPort发送到mainIsolate`
  3. 释放newIsolate

    • 当我们使用完自己创建的Isolate之后,最好调用kill将Isolate杀死,否则Isolate 会一直存在造成内存消耗
  4. Platform-Channel 通信仅仅主 isolate 支持。该主 isolate 对应于应用启动时创建的 isolate

    • 也就是说,通过编程创建的 isolate 实例,无法实现 Platform-Channel 通信 这难道是

区别

  • swpanUrinewIsolate 必须在单独的文件里,又因为必须有main函数作为入口,所以程序会出现两个main

  • 在flutter环境下一直又一个main的错误未处理 我猜测可能是因为两个main函数引起的,也有可能是上面第四点原因导致的如果有人知道请评论告诉我下 谢谢了

  • spawn 通常我们newIsolatemainIsolate写在同一个文件,也不会出现两个main函数方便管理Isolate

  • 在flutter环境下spwan创建可以正常执行

    • spwan要注意入口点函数必须是顶层函数或者静态函数

在flutter中创建Isolate

可以看到 无论是实现上述哪一种isolate 代码数量都是比较繁琐的

对此Flutter提供了一个函数**compute** 该函数封装了通过spawn实现Isolate的代码

避免我们去写通过spawn创建Isolate的一系列代码,

直接通过compute函数,这样让我们的代码看起来更简洁了

compute

如果任务只是进行一次计算返回结果,不需要双端多次沟通的话 使用compute 函数将非常简单

compute(ComputeCallback callback, Q message)

  • compute函数有两个必须的参数

    • callback 为执行函数
      • 必须是顶级函数或者静态方法
    • message为消息 可以是callback执行函数的参数
  • 示例

    • import 'dart:io';
      
      import 'package:flutter/foundation.dart';
      import 'package:flutter/material.dart';
      
      void main() => runApp(new MyApp());
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return new MaterialApp(
              title: 'ComputeIsolate',
              theme: new ThemeData(
                primarySwatch: Colors.blue,
              ),
              home: Scaffold(
                  appBar: AppBar(title: Text("ComputeIsolate")),
                  body: Column(
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Center(
                          child: FlatButton(
                              onPressed: () async {
                                String s = await compute(work, 4);
                                print(s);
                              },
                              child: Text(
                                "Click",
                              )),
                        ),
                        Center(
                            child: Text(
                          "Click",
                        )),
                      ])));
        }
      }
      String work(num duration) {
        print("work start");
        sleep(Duration(seconds: duration));
        return "$duration 秒后执行结束";
      }
      
      // 运行点击屏幕 后输出
      // work start
      // 4 秒后执行结束
      
  • 怎么样 是不是很简单

我们应该什么时候使用Future和Isolate

  • 耗时较多的任务放到Isolate中
    • 如果你不想你的Ui卡顿或者程序中断
      • 通过之前的时间循环机制我们了解到Future只是将任务放到了Event队列,还是在当前Isolate 不过是等其他代码执行结束在执行Event队列中的任务,而UI渲染交互等又都是在Event队列中处理,如果我们Future任务耗时过多会导致Ui卡顿甚至整个进程都被中断,所以我们才需要多Isolate 并发处理任务
    • 直观的说可以根据任务执行时间长短来区分
      • 代码段运行时间只要几十毫秒=>Future
      • 代码段运行时间要几百毫秒甚至更久应该用Isolate
        • 比如Io操作

总结

  • Isolate虽然可以并发但是也要考虑适用场景
  • 如果需要使用Isolate 要考虑场景
    • 只执行一次返回结果 不需要多次通讯 使用compute函数
    • 如果需要多次沟通我们可以通过 spawn 来创建Isolate

参考

掘金文章 [译] Flutter 异步编程:Future、Isolate 和事件循环

你可能感兴趣的:(Dart,多线程,Isolate,dart)