深入学习Dart线程模型

举报
IT编程技术学习栈 发表于 2023/03/29 15:19:25 2023/03/29
【摘要】 Java和OC都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但Dart和JavaScript不会,它们都是单线程模型,运行机制很相似(但有区别)。

Dart 单线程模型

Java和OC都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但Dart和JavaScript不会,它们都是单线程模型,运行机制很相似(但有区别)。

Dart在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。微任务队列的执行优先级高于事件队列。


Dart大致运行原理:先开启app执行入口函数main(),执行完成之后,消息机制启动,先是会按照先进先出的顺序逐个执行微任务队列中的任务microtask,事件任务eventtask执行完毕后便会退出,但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而Flutter中,主线程的执行过程正是如此,永不终止。

在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。


Dart 中事件的执行顺序:Main > MicroTask > EventQueue

通常使用 scheduleMicrotask(…)或者Future.microtask(…)方法向微任务队列插入一个任务。

通常使用 Future 向 EventQueue加入事件,也可以使用 async 和 await 向 EventQueue 加入事件。

例如:

void main(){
  run();
}

void run(){

  Future(() async {

    print("EvenTask1");

  });

  evenTest();

  Future(() async{

    print("EvenTask2");

    await Future((){

      print("EvenTask4");

    });

    print("EvenTask3");

  });

  Future.microtask(() => print("MicroTask1"));

  scheduleMicrotask((){

    print("MicroTask2");

  });

  print("main");

}

void evenTest() async{

  await Future((){

    print("EvenTask5");

  });

}

输出结果是:

main
MicroTask1
MicroTask2
EvenTask1
EvenTask5
EvenTask2
EvenTask4
EvenTask3

在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。

深入理解Flutter及Dart单线程模型

众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁

而 Dart 则是一种单线程语言,单线程语言就意味着代码执行顺序是有序的,下面结合一个demo深入了解单线程模型。


Demo示例:

点击一个按钮,会调用如下方法,读取一个约 2M 大小的 json 文件。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
}

点击刷新按钮之后,如下图所示,中间的 loading 会卡一下。很多同学一看这个代码就知道,肯定会卡,解析一个 2M 的文件,而且是同步解析,主页面肯定是会卡的。

那如果我换成异步解析呢?还卡不卡?大家可以脑海中思考下这个问题。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  // 异步解析
  Future(() {
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
  }).then((value) {});
}

大家可以看到,我已经放在异步里解析了,为什么还是会卡呢?大家可以先思考下这个问题。

前面已经提到了 Dart 是一种单线程语言,单线程语言就意味着代码执行顺序是有序的。当然 Dart 也是支持异步的。这两点其实并不冲突。

Dart线程解析

我们来看看 Dart 的线程,当我们 main() 方法启动之后,Dart已经开启了一个线程,这个线程的名字就叫 Isolate。每一个 Isolate 线程都包含了图示的两个队列,一个 Microtask queue,一个 Event queue。


Isolate 线程会优先执行 Microtask queue 里的事件,当 Microtask queue 里的事件变成空了,才会去执行 Event queue 里的事件。如果正在执行 Microtask queue 里的事件,那么 Event queue 里的事件就会被阻塞,就会导致渲染、手势响应等都得不到响应(绘制图形,处理鼠标点击,处理文件IO等都是在 Event Queue 里完成)。

所以为了保证功能正常使用不卡顿,尽量少在 Microtask queue 做事情,可以放在 Event queue 做。


为什么单线程可以做一个异步操作呢?

因为 APP 只有在你滑动或者点击操作的时候才会响应事件。没有操作的时候进入等待时间,两个队列里都是空的。这个时间正是可以进行异步操作的,所以基于这个特点,单线程模型可以在等待过程中做一些异步操作,因为等待的过程并不是阻塞的,所以给我们的感觉就像同时在做多件事情,但自始至终只有一个线程在处理事情。


Future

当方法加上 async 关键字,就代表这个方法开启了一个异步操作,如果这个方法有返回值,就必须要返回一个 Future。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  // 异步解析
  Future(() {
    ...
  }).then((value) {});
}

一个 Future 异步任务的执行,相对简单。在我们声明一个 Future 之后,Dart 会将异步里的代码函数体放在 Event queue 里执行然后返回。这里注意下,Future 和 then 是放在同一个 Event queue 里的。


假设,我执行 Future 代码之后没有立即执行 then 方法,而是等 Future 执行之后5秒,才调用 then 方法,这时候还是放在同一个 Event queue 里吗?显然是不可能的,我们看一下源码是怎么实现的。

Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
  ...
  _addListener(new _FutureListener<T, R>.then(result, f, onError));
  return result;
}

bool get _mayAddListener => _state <= (_statePendingComplete | _stateIgnoreError);

void _addListener(_FutureListener listener) {
  assert(listener._nextListener == null);
  if (_mayAddListener) {
    // 待完成
    listener._nextListener = _resultOrListeners;
    _resultOrListeners = listener;
  } else {
    // 已完成
    ...
    _zone.scheduleMicrotask(() {
      _propagateToListeners(this, listener);
    });
  }
}

可以看到 then 方法里有一个监听,Future 执行之后5秒才调用,很明显是已完成状态,走 else 那里的 scheduleMicrotask() 方法,就是说把 then 里面的方法放到 Microtask queue


Future 为何卡顿

再来说一下刚刚的问题,我已经放在异步里解析了,为什么还是会卡呢?

其实很简单,Future 里的代码可能需要执行10s,也就是 Event queue 需要10s才能执行完。那这个10s内其他代码肯定就无法执行了。所以 Future 里的代码执行时间过长,还是会卡 UI 的。


以 Android 为例,Android的刷新频率是60帧/秒,Android系统中每隔16.6ms会发送一次 VSYNC(同步)信号,触发UI的渲染。所以我们就要考虑下,一旦代码执行时间超过16.6ms,到底应不应该放在 Future 里执行?

这时候是不是有同学有疑问,我网络请求也是用 Future 写的,为什么就不卡呢?

这个大家就需要注意一下,网络请求不是放在 Dart 层面执行的,它是由操作系统提供的异步线程去执行的,当这个异步执行完系统又返回给 Dart。所以即使 http 请求需要耗时十几秒,也不会感到卡顿。


compute

既然 Future 执行也会卡顿,那要怎么去优化呢?这时候我们可以开一个线程操作,Flutter 为我们封装好了一个 compute()方法,这个方法可以为我们开一个线程。我们用这个方法来优化一下代码,然后再看下执行效果。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  var result = compute(parse,jsonStr);
}

static VideoListModel parse(String jsonStr){
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  return VideoListModel.fromJson(json.decode(jsonStr));
}

可以看到此时点击刷新按钮,已经不再卡顿了。遇到一些耗时的操作,这确实是一种比较好的解决方式。


我们再看看 DefaultAssetBundle.of(context).loadString(“assets/list.json”) 方法里面是怎么执行的。

Future<String> loadString(String key, { bool cache = true }) async {
  final ByteData data = await load(key);
  if (data == null)
    throw FlutterError('Unable to load asset: $key');
  // 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
  // on a Pixel 4.
  if (data.lengthInBytes < 50 * 1024) {
    return utf8.decode(data.buffer.asUint8List());
  }
  // For strings larger than 50 KB, run the computation in an isolate to
  // avoid causing main thread jank.
  return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}


从官方源码可以看到,当文件的大小超过 50kb 时,也是采用 compute() 方法开一个线程去操作的。

多线程机制

Dart 作为一个单线程语言,虽然提供了多线程的机制,但是在多线程的资源是隔离的,两个线程之间资源是不互通的。

Dart 的多线程数据交互需要从 A 线程传给 B 线程,再由 B 线程返回给 A 线程。而像 Android 在主线程开一个子线程,子线程可以直接拿主线程的数据,而不用让主线程传给子线程。

总结

1.Future 适合耗时小于 16ms 的操作

2.可以通过 compute() 进行耗时操作

3.Dart 是单线程原因,但也支持多线程,但是线程间数据不互通

4.Isolate 线程会优先执行 Microtask queue 里的事件,当 Microtask queue 里的事件变成空了,才会去执行 Event queue 里的事件。如果正在执行 Microtask queue 里的事件,那么 Event queue 里的事件就会被阻塞,就会导致渲染、手势响应等都得不到响应(绘制图形,处理鼠标点击,处理文件IO等都是在 Event Queue 里完成)。

5.Dart 中事件的执行顺序:Main > MicroTask > EventQueue

6.用await和Future包住的代码都是在eventQueue里运行的,同时没有延迟的Future代码的then也是在同一个eventQueue里执行的。只有延迟的代码Future和then会分开处理,此时then进入到了microTask队列里。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。