一架梯子,一头程序猿,仰望星空!

Dart Isolate


我们知道一般常用并发机制主要包括进程、线程以及后面的协程,在Dart 语言中并发机制由Isolate实现,Isolate你可以简单的理解是一种特殊的线程。

Isolate的特点:

  • Isolate之间不共享内存。
  • Isolate之间只能通过消息通讯。

不共享内存,意味着你不能像线程那样通过变量共享状态,每个Isolate都有自己独立的内存,这样设计的好处就是你不用加锁,也能安全的操作自己的数据。

1.为什么需要多个Isolate实现并发处理

默认情况Dart启动一个Isolate,main函数就是他的入口,这就是为什么说Dart是单线程,默认只是启动一个Isolate, 相当于程序是单线程执行的。

我们通过异步机制,可以同时处理多个接口请求之类的异步任务,不是也有类似并发的效果吗?那么为什么还需要Isolate并发机制。

首先,一个Isolate不能利用多个CPU, 再则异步机制只是适合用于处理各种网络IO,这些网络IO不怎么消耗CPU资源,只是需要大量的等待请求响应的时间,因此我们可以利用等待的空闲时间去处理其他任务,这就是异步机制可以提高性能的原因。但是如果你有一个计算量非常大的任务,例如,你有大量的数据要处理,需要对视频进行格式化处理 ,这个时候这些CPU密集型计算会阻塞你的线程,其他任务都执行不了了。

针对比较消耗CPU的任务,最好创建一个新的 Isolate 去处理,避免阻塞主 Isolate (也就是主线程),这样可以利用设备的多核特性。

2. Isolate 基本用法

通过例子了解Isolate基本用法。

// 导入isolate包
import 'dart:isolate';

void main() {
        // 通过Isolate.spawn静态函数,创建一个新的Isolate
        // spawn是一个泛型函数,接受一个泛型参数,表示Isolate入口函数接受的参数类型
       // 这里spawn的泛型参数是String,subTask是入口函数
       // 第二个参数跟泛型参数类型一致,表示传递给入口函数的参数,这里传入的是字符串。
	Isolate.spawn<String>(subTask, "Task1 parameter");
        
        // main函数结束标记 
	print("main func end.");
}

// Isolate入口函数定义,接受一个String参数
// 入口函数的参数类型由上面的spawn的泛型参数决定。
void subTask(String msg) {
	print("subTask recv: $msg");
}

输出结果:

main func end.
subTask recv: Task1 parameter

通过输出,我们发现先打印了 main func end. 然后,执行新建Isolate的入口函数。 如果我们想让代码执行顺序,跟我们书写顺序一致的话,可以使用await关键词等待Isolate执行结束

例子改写为:

// 导入isolate包
import 'dart:isolate';

// 使用async关键词将main函数标记为一个异步函数,这样才能使用await关键词
void main() async {
        // 使用await关键词等待任务执行完成
	await Isolate.spawn<String>(subTask, "Task1 parameter");
        
        // main函数结束标记 
	print("main func end.");
}

// Isolate入口函数定义,接受一个String参数
void subTask(String msg) {
	print("subTask recv: $msg");
}

输出:

subTask recv: Task1 parameter
main func end.

这个时候输出顺序跟我们代码书写顺序一致了。

3.Isolate 消息通讯

多个Isolate 之间只能通过消息通讯,例如,我们如何获取一个Isolate的计算结果。

主要通过ReceivePort和SendPort两个类处理消息通讯。

ReceivePort 负责接收 SendPort 发送的消息, SendPort 和 ReceivePort 是捆绑关系, SendPort 是由 ReceivePort 创建的。

例子:

// 导入isolate包
import 'dart:isolate';

void main() async {
    // 创建一个ReceivePort用于接收消息
    var recv = ReceivePort();

    // 创建一个Isolate,泛型参数为SendPort,入口函数为subTask
    // subTask入口函数的参数为SendPort类型,因此spawn第二个参数,传入recv的sendPort对象。
	Isolate.spawn<SendPort>(subTask, recv.sendPort);
        
    // 使用await等待recv的第一条消息
    var result = await recv.first;

    print("recv: $result");
}

// Isolate入口函数定义,接收一个SendPort对象作为参数
void subTask(SendPort port) {
    // 使用SendPort发送一条字符串消息
	port.send("subTask Result.");
}

输出:

recv: subTask Result.