11.Dart 使用Isolates以及Worker

·  阅读 130

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方法的源码

Future<Isolate> spawn<T>(
void entryPoint(
T message
),
T message,
{bool paused = false,
bool errorsAreFatal = true,
SendPort? onExit,
SendPort? onError,
@Since("2.3") String? debugName}
)
复制代码

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

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!!');
}
复制代码

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';

num? i;

void main() {
  i = 2;
  late SendPort childSendPort;
  print('主函数main()因为给i赋值了$i');
  //创建一个消息接收器--这里创建的是默认的main的isolate的,我们可以称之为主进程
  //创建新的具有发送器的isolate,第一个参数是具有内存隔离的新的isolate的具体业务逻辑函数,第二个是创建的isolate的时候传递的参数,一般我们传递当前isolate的发送器
  ReceivePort rp = new ReceivePort();
  Isolate.spawn(isoVice, rp.sendPort);
  //主进程接受持有主进程发送器的isolate发过来的消息
  rp.listen((message) {
    //其他的isolate可以选择发过来自身的sendPort给主进程,则主进程isolate也可以向创建的isolate发送消息,完成交互操作
    if (message is SendPort) {
      childSendPort = message;
      message.send('=====接收到函数isoVice()的发送器=====');
    } else {
      print("接到函数isolateVice()消息:" + message);
      if (childSendPort != null) {
        childSendPort.send('您喜欢我哪些,我改还不成嘛!'); //进行一次回复
      }
    }
  });
}

/// 内存隔离的新的isolate的具体业务逻辑函数
void isoVice(SendPort sp) {
  // isolate是内存隔离的,i的值是在其他isolate定义的(默认都是主isolate环境)所以这里获得null
  print('并发isoVice()没有给i赋值,所以输出i=' + i.toString());
  ReceivePort rp = ReceivePort(); //当前isolate的消息接收器
  sp.send(rp
      .sendPort); //创建当前函数(isolateVice)的时候传递的第二个参数(这里我们认为是该iso的发送器),使用主iso的发送器将自身子iso的发送器发送过去,完成交互
  sp.send('我喜欢你'); // 测试向主isolate发送消息

  rp.listen((message) {
    print('接收到函数main()消息:' + message);
  });
}
复制代码

事件驱动

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

从上图中我们可以很清晰的看出来两个队列之间的微妙的执行流程:

  • 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开发中也格外重要。

分类:
前端
标签:
分类:
前端
标签: