Dart 使用isolates以及Worker

948 阅读8分钟

Dart 使用isolates以及Worker> 当有计算很繁重的任务时,则需要使用isolate或者Worker来执行,以保持App对用户操作的及时响应。Isolate的实现可能是一个单独的线程,或者一个单独的进程,需要看Dart VM是如何实现的

大多数计算机中,甚至在移动平台上,都在使用多核CPU。 为了有效利用多核性能,开发者一般使用共享内存数据来保证多线程的正确执行。 然而, 多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。

  • 所有 Dart 代码都在_隔离区_( isolates )内运行,而不是线程
  • 每个隔离区都有自己的内存堆,确保每个隔离区的状态都不会被其他隔离区访问

**

并发(Concurrency)

Concurrency是同时执行多个指令序列。 它涉及同时执行多个任务 我们知道dart是个单线程的语言,和js一样,所以dart中不存在多线程操作,那么我们如果遇到多任务并行的场景,该如何去做呢? Dart中提供了一个**类似于java新线程但不能共享内存的独立运行的worker ,属于一个新的独立的Dart执行环境isolate **作为并行工作的工具, Isolates,顾名思义,是运行代码的独立单元。 在它们之间发送数据的唯一方法是传递消息,就像在客户端和服务器之间传递消息的方式一样。 isolate可帮助程序充分利用多核微处理器的优势, 像我们执行任务的时候默认的main方法就是一个默认的isolate,可以看出如果我们想在dart中执行多个并行的任务,可以选择创建多个isolate来完成 **dart:isolate **包是Dart的解决方案,用于获取单线程Dart代码并允许应用程序更多地使用可用的硬件 那么isolate之间如何交互?isolate自身的任务又是如何处理的?

Isolate.spawn

spawn方法的源码

spawn<T>( 
    void entryPoint(T message), 
    T message, { 
        bool paused: false, 
        bool errorsAreFatal, 
        SendPort onExit, 
        SendPort onError 
    } 
) → Future<Isolate>//可以看出来最终会返回一个Isolate对象,至于Future是什么,接下来会介绍

让我们举个例子来更好地理解这个概念

import 'dart:isolate';

void foo(var message) {
  print('execution from foo ... the message is :${message}');
}

void main() {
  Isolate.spawn(foo, 'Hello!!');
  Isolate.spawn(foo, 'Greetings!!');
  Isolate.spawn(foo, 'Welcome!!');
  print('execution from main1');
  print('execution from main2');
  print('execution from main3');
}

这里, Isolate类的spawn方法有助于与我们的其余代码并行运行函数foospawn函数有两个参数

  • 产生的功能,和
  • 将传递给衍生函数的对象
  • 如果没有对象传递给生成的函数,则可以传递NULL值

这两个函数(foo and main)可能不一定每次都以相同的顺序运行。 无法保证foo何时执行以及何时执行main()。 每次运行时输出都不同

一般来说我们使用spawn创建的isolate自身带有控制接口(control port )和可控制对象的能力(capability ),当然我们也可以不拥有这个能力,那么isolate之间是如何进行交互操作的?我们看下流程图 isolate交互.png

从上图中我们可以看到两个isolate之间使用SendPort相互发送消息,而isolate中也存在了一个与之对应的ReceivePort接受消息用来处理,但是我们需要注意的是,ReceivePort和SendPort在每个isolate都有一对,只有同一个isolate中的ReceivePort才能接受到当前类的SendPort发送的消息并且处理(可以看出来谷歌这么设计的意义就是防止多个isolate之间接受器混乱),而isolate的spawn就是用来创建带有控制能力的isolate,第二个参数就可以选择传递当前Isolate的SendPort,交给新创建的实例,这样新创建的实例就可以发送消息给原来的isolate,实现两者之间通讯,接下来我们看一下Isolate交互的实例:

import 'dart:isolate';

int i;

void main() {
  i = 10;
  SendPort childSendPort;
  print("主函数main()因为给i赋值了10,所以输出i = " + i.toString());
  //创建一个消息接收器--这里创建的是默认的main的isolate的,我们可以称之为主进程
  //创建新的具有发送器的isolate,第一个参数是具有内存隔离的新的isolate的具体业务逻辑函数,第二个是创建的isolate的时候传递的参数,一般我们传递当前isolate的发送器
  ReceivePort receivePort = new ReceivePort();
  Isolate.spawn(isolateVice, receivePort.sendPort);

  //主进程接受持有主进程发送器的isolate发过来的消息
  receivePort.listen((message) {
    //其他的isolate可以选择发过来自身的sendPort给主进程,则主进程isolate也可以向创建的isolate发送消息,完成交互操作
    if (message is SendPort) {
      childSendPort = message;
      message.send("———————— 已收到函数isolateVice()的发送器 ————————");
    } else {
      print("接到函数isolateVice()消息:" + message);
      if (childSendPort != null) {
        childSendPort.send('您喜欢我哪些,我改还不成嘛!'); //进行一次回复
      }
    }
  });
}

/// 内存隔离的新的isolate的具体业务逻辑函数
void isolateVice(SendPort sendPort) {
  // isolate是内存隔离的,i的值是在其他isolate定义的(默认都是主isolate环境)所以这里获得null
  print("并发isolateVice()没有给i赋值,所以输出i = " + i.toString()); //输出:--->null

  ReceivePort receivePort = new ReceivePort();   //当前isolate的消息接收器
  //创建当前函数(isolateVice)的时候传递的第二个参数(这里我们认为是该iso的发送器),使用主iso的发送器将自身子iso的发送器发送过去,完成交互
  sendPort.send(receivePort.sendPort);

  sendPort.send("我喜欢你"); // 测试向主isolate发送消息
  receivePort.listen((message) {
    print("接到函数main()消息::" + message);
  });
}

事件驱动

我们在上面有提到,每一个isolate相当于一个完全独立的dart执行环境,那么当前的环境中如果存在一些任务,如果完全按照顺序执行,岂不是会因为某个任务处理的时间过于久,后面的任务来不及执行?在单线程语言中,如果不做任何处理,的确会出现这种问题,熟悉js的人都知道js的异步操作很优秀,原因在于js有着不错的事件驱动进行任务调度,提高任务执行的效率,尤其是最近热门的node.js,更是把事件驱动做到极致,而dart作为一个优秀的单线程语言,自然不可能缺少事件驱动这个优秀的特性,在dart中事件驱动是存在于isolate中的,也就是说,我们每一个新的isolate都有一个独立的完整的event-loop,而每一个Loop中又包含了两个队列,其中一个队列叫microtask queue(微任务队列),该队列的执行优先级最高,而另外一个队列**event queue(事件队列)**属于普通的事件队列,两个队列依靠着固定的执行流程完成了整个的dart任务执行机制,事件驱动的执行流程图如下: image.png isolate中的消息机制.png 从上图中我们可以很清晰的看出来两个队列之间的微妙的执行流程:

  • microtask-queue的执行优先于event-queue,并且在microtask-queue中是轮询执行的,也就是说,microtask-queue中所有的任务执行完成以后才会去event-queue中执行任务
  • event-queue中的任务执行级别最低,每一个任务执行完毕以后,都会重新去轮询一次microtask-queue,如果这个时候microtask-queue中有了新的任务,那么不用说,肯定会把microtask-queue再次全部执行完再回到event-queue中执行

并且我们平时执行的任务绝大多数都是在event-queue中执行的,所以我们正常的任务如果想要执行,microtask-queue中尽量不要加入太多复杂的业务操作,但同时我们也可以看出来,dart中存在着‘插队’机制,即我们希望某个任务优先于其他任务先去执行,我们可以选择将任务丢进microtask-queue优先处理,接下来我们先看一个案例:

import 'dart:io';

void main(){
  new File("C:\Users\Administrator\Desktop\http通用类.txt").readAsString().then((content){
      print(content);//按理来说应该会输出文件中每一行的数据的,但是一直不输出
  });
  while(true){}
}

从上面的案例的结果可以看出来,程序一直阻塞着,永远不执行文件io输出的内容,这是为什么呢?原因很简单,因为io流是异步的操作,并且than方法会把任务加入到event-queue,这个时候main函数的while循环早于文件io执行,就会一直阻塞程序,所以在dart中合理分配microtask-queue和event-queue很重要,同样因为我们这里用了异步的任务,导致了任务队列执行顺序的变化,所以合理运用同步任务和异步任务在dart开发中也格外重要。接下来我们学习dart中的异步任务和执行器Future

image.png有关更多信息,请参考 dart:isolate library documentation