注册

【Flutter基础】Dart中的并发Isolate

前言


说到 Flutter 中的异步,我想大家都不陌生。一般我们使用 Futureasync-await 来进行网络请求、文件读取等异步加载,但要提到 Isolate ,大家就未必能够说的明白了,今天我就带大家了解下 Dart 中的并发 Isolate


一、Isolate的基本用法


1.1 Isolate的基本用法


对于 Isolate ,我们一般通过 Isolate.spawn() 来实现并发处理。


  const String downloadLink = '下载链接';
 final resultPort = ReceivePort();
 await Isolate.spawn(readAndParseJson, [resultPort.sendPort, downloadLink]);
 String fileContent = await resultPort.first as String;
 print('展示文件内容: $fileContent');

Isolate.spawn() 内部传递一个 entryPoint 初始化函数,用于执行异步操作。这里我们定义 readAndParseJson 函数,通过设置 延迟2秒 来模拟文件读取。


Future<void> readAndParseJson(List<dynamic> args) async {
 SendPort resultPort = args[0];
 String fileLink = args[1];

 print('获取下载链接: $fileLink');

 String fileContent = '文件内容';
 await Future.delayed(const Duration(seconds: 2));
 Isolate.exit(resultPort, fileContent);
}

运行结果:


WX20230314-232225@2x.png


1.2 Isolate的异常处理


由于 Isolate 开启的是一块新的隔离区,完全和启动的 Isolate 独立,自然是无法通过 try-catch 进行捕获。


好在 Isolate 提供了异常通知的能力,我们依旧可以通过 ReceivePort 来接收 Isolate 产生的异常,代码如下所示:


  const String downloadLink = '下载链接';
 final resultPort = ReceivePort();

 await Isolate.spawn(
   readAndParseJsonWithErrorHandle,
  [resultPort.sendPort, downloadLink],
   onError: resultPort.sendPort,
   onExit: resultPort.sendPort,
);

 // 获取结果
 final response = await resultPort.first;
 if (response == null) { // 没有消息
   print('没有消息');
} else if (response is List) { // 异常消息
   final errorAsString = response[0]; //异常
   final stackTraceAsString = response[1]; // 堆栈信息
   print('error: $errorAsString \nstackTrace: $stackTraceAsString');
} else { // 正常消息
   print(response);
}

readAndParseJsonWithErrorHandle 函数中,我们通过手动 throw Exception 来触发异常处理。


Future<void> readAndParseJsonWithErrorHandle(List<dynamic> args) async {
 SendPort resultPort = args[0];
 String fileLink = args[1];
 String newLink = '文件链接';

 await Future.delayed(const Duration(seconds: 2));
 throw Exception('下载失败');
 Isolate.exit(resultPort, newLink);
}

运行结果:


WX20230315-212149@2x.png


1.3 Isolate.run()


如果我们每次使用时都需要通过 ReceivePort 来实现 Isolate 的消息通信,这样会过于繁琐。好在官方也考虑到了这个问题,通过提供 Isolate.run() 来直接获取返回值:



注意:该方法需要在 Dart 2.19 以上的版本使用,对应 Flutter 3.7.0 以上。



 const String downloadLink = '下载链接';
String fileContent = await Isolate.run(() => handleReadAndParseJson(downloadLink));
print('展示文件内容: $fileContent');

/// 处理读取并解析文件内容
Future<String> handleReadAndParseJson(String fileLink) async {
 print('获取下载链接: $fileLink');
 String fileContent = '文件内容';
 await Future.delayed(const Duration(seconds: 2));
 return fileContent;
}

其原理就是内部通过对 Isolate.spawn() 进行封装,通过 Completer 来实现 Future 的异步回调。关键代码如下:


    var result = Completer<R>();
   var resultPort = RawReceivePort();
  ...
   try {
     Isolate.spawn(_RemoteRunner._remoteExecute,
             _RemoteRunner<R>(computation, resultPort.sendPort),
             onError: resultPort.sendPort,
             onExit: resultPort.sendPort,
             errorsAreFatal: true,
             debugName: debugName)
        .then<void>((_) {}, onError: (error, stack) {
       // Sending the computation failed asynchronously.
       // Do not expect a response, report the error asynchronously.
       resultPort.close();
       result.completeError(error, stack);
    });
  } on Object {
     // Sending the computation failed synchronously.
     // This is not expected to happen, but if it does,
     // the synchronous error is respected and rethrown synchronously.
     resultPort.close();
     rethrow;
  }


Tip:从官方的源码中我们可以学到,在调用 Isolate.spawn() 时,建议通过 tray-catch 捕获可能发生的异常,并且在最后需要关闭 ReceivePort 避免内存泄漏。



二、Flutter中的compute


除了上述在 Dart 中的用法外,我们还可以在 Flutter 中通过 compute() 来实现。并且这也是官方推荐的用法,因为 compute() 允许在非原生平台 Web 上运行。



官方原文:If you’re using Flutter, consider using Flutter’s compute() function instead of Isolate.run(). The compute function allows your code to work on both native and non-native platforms. Use Isolate.run() when targeting native platforms only for a more ergonomic API.



2.1 compute的使用


Isolate.run() 的使用方式类似,通过传入 callback 函数让 Isolate 执行:


  String content = await compute((link) async {
   print('开始下载: $link');
   await Future.delayed(const Duration(seconds: 2));
   return '下载的内容';
}, '下载链接');
 print('完成下载: $content');

运行结果:


WX20230315-223530@2x.png



Tip:在引入 Flutter 包之前,我们可以直接右键 run 'islate.dart' with Coverage 在Coverage 运行;在引入 Flutter 包之后,我们就需要在手机上运行,可以通过命令:open -a simulator 启动一个 iOS 模拟器运行。



2.2 compute的异常处理


查看 compute() 源码发现,内部使用的是 Isolate.run(),而 Isolate.run() 内部是通过 Completer 来完成异步回调的。因此,我们直接通过 try-catch 即可捕获异常:


  try {
   await compute((link) async {
     await Future.delayed(const Duration(seconds: 2));
     throw Exception('下载失败');
  }, '下载链接');
} catch (e) {
   print('error: $e');
}
 print('结束');

运行结果:


WX20230315-225157@2x.png


2.3 compute的源码分析


compute() 实际是对 isolates.compute 的实例化:


const ComputeImpl compute = isolates.compute;

Isolates 却是通过不同的平台来指定引入的类,这样也印证了为什么官方推荐在 Flutter 中使用 compute()


import '_isolates_io.dart'
 if (dart.library.js_util) '_isolates_web.dart' as isolates;

_isolates_io.dart 中是通过 Isolate.run() 来实现:


Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, {String? debugLabel}) async {
 debugLabel ??= kReleaseMode ? 'compute' : callback.toString();

 return Isolate.run<R>(() {
   return callback(message);
}, debugName: debugLabel);
}

_isolates_web.dart 中是通过 await null; 抽取单帧来执行函数:


Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
 // To avoid blocking the UI immediately for an expensive function call, we
 // pump a single frame to allow the framework to complete the current set
 // of work.
 await null;
 return callback(message);
}

2.4 compute小结



  1. 因为 compute() 需要引入 flutter/foundation.dart,所以只能在 Flutter 中运行。
  2. 在 Flutter 中推荐使用 compute() 来实现,因为兼容 Web 平台。
  3. 其内部实现:在平台侧通过 Isolate.run(),在 Web 侧通过 await null; 抽取单帧来执行函数。

三、Isolate 的工作原理


在 Dart 中,Isolate 是一种类似于线程的概念,可以独立于其他 Isolate 运行,并且具有自己的堆栈和内存空间。这使得 Isolate 可以并行执行代码,并且不会受到其他 Isolate 的影响。


3.1 Future 为何还是会导致卡顿?


有时候我们可能会困惑,为什么明明已经使用了 Future 来异步执行任务,还是会出现卡顿的现象。那是因为 Dart 是单线程的,如果在执行 Future 时遇到耗时的计算任务或者 I/O操作,这些操作会占用当前线程的资源,从而导致应用出现卡顿现象,影响用户体验。


相比之下,Isolate 可以实现多线程并发执行任务,可以利用多核 CPU,因此可以更有效地处理大规模的计算密集型任务、I/O 密集型任务以及处理需要大量计算的算法等。在 Isolate 中执行任务不会占用 UI 线程的资源,从而可以保证应用的流畅性和响应速度。


3.2 Isolate 的工作原理


Isolate 的工作原理是通过使用 Dart 的隔离机制来实现的。每个 Isolate 都运行在独立的隔离环境中,并且与其他 Isolate 共享代码的副本。这意味着Isolate之间不能直接共享数据,而必须使用消息传递机制来进行通信。


其实我们在执行 main() 时,就开始了主 Isolate 的运行,如下图所示:


basics-main-isolate.png


3.3 Isolate 的生命周期


Isolate的生命周期可以分为三个阶段:创建、运行和终止。



  1. 创建阶段:使用 Isolate.spawn() 方法可以创建一个新的 Isolate,并且将一个函数作为参数传递给这个方法。这个函数将作为新的 Isolate 的入口点,也就是 Isolate 启动时第一个执行的函数。创建 Isolate 时还可以指定其他参数,例如 Isolate 的名称、是否共享代码等等。
  2. 运行阶段:一旦创建了 Isolate,它就会开始执行入口点函数,并且进入事件循环。在事件循环中,Isolate 会不断地从消息队列中获取消息,并且根据消息的类型执行相应的代码。Isolate 可以同时执行多个任务,并且可以通过消息传递机制来协调这些任务的执行顺序。
  3. 终止阶段:当 Isolate 完成了它的任务,或者由于某些原因需要停止时,可以调用 Isolate.kill() 方法来终止 Isolate。此时,Isolate 会立即停止执行,并且 Isolate 对象和所有与它相关的资源都会被释放。

basics-isolate.png


3.4 Isolate 组


Dart 2.15 也就是 Flutter 2.8 版本之后,当一个 Isolate 调用了 Isolate.spawn(),两个 Isolate 将拥有同样的执行代码,并归入同一个 Isolate 组 中。Isolate 组会带来性能优化,例如新的 Isolate 会运行由 Isolate 组持有的代码,即共享代码调用。同时,Isolate.exit() 仅在对应的 Isolate 属于同一组时有效。


其原理是同一个 Isolate 组中的 Isolate 共享同一个堆,避免了对象的重复拷贝。这意味着生成一个新 Isolate 的速度提高了 100 倍,消耗的内存减少了 10-100 倍。



注意不要和前面的概念混淆,Isolate 仍然无法彼此共享内存,仍然需要消息传递。



四、使用场景


4.1 使用原则



  • 如果一段代码不会被中断,那么就直接使用正常的同步执行就行。
  • 如果代码段可以独立运行而不会影响应用程序的流畅性,建议使用 Future
  • 如果繁重的处理可能要花一些时间才能完成,而且会影响应用程序的流畅性,建议使用 Isolate

4.2 耗时衡量


通过原则来判断可能过于抽象,我们可以用耗时来衡量:



  • 对于耗时不超过 16ms 的操作推荐使用 Future
  • 对于耗时超过 16ms 以上的操作推荐使用 Isolate

至于为什么用 16ms 作为衡量呢,因为屏幕一帧的刷新间隔就是 16ms



compute API文档原文:


/// {@template flutter.foundation.compute.usecase} /// This is useful for operations that take longer than a few milliseconds, and /// which would therefore risk skipping frames. For tasks that will only take a /// few milliseconds, consider [SchedulerBinding.scheduleTask] instead. /// {@endtemplate}



五、结语


至此,我们完成了对 Isolate 概念和用法的认识。项目源码:Fitem/flutter_article


如果觉得这篇文章对你有所帮助的话,不要忘了一键三连哦,大家的点赞是我更新的动力🥰。最后祝大家周末愉快~



参考资料:


Isolate 的工作原理


Isolates in Flutter


作者:Fitem
链接:https://juejin.cn/post/7211539869805805623
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册