likes
comments
collection
share

Dart 的同步与异步

作者站长头像
站长
· 阅读数 13

我们都知道 Dart 是一个单线程模型的语言,但这并不意味着它只支持单线程,它也是支持多线程的,可以使用 isolate 进行实现的。所以,这个单线程的说法具有迷惑性,实际情况下,其单线程的执行只是形容它的事件循环机制,至于 IO、网络等操作,还是得放到 isolate 中。而 main 方法的执行,其实也是在一个 isolate 中。 所以,若是仅仅是说事件循环机制,使用 Java 也是同样可以模拟一个出来,只是 Android 提供了 Handler 机制去解决事件分发处理的问题,不需要我们自己重新封装。

事件循环机制

简单来说,代码的执行是顺序执行,从 main() 开始,等遇到 Microtask(微任务) 和 Timer(事件) 后,就分别用队列将其存储起来,当 main 方法执行完后,就会先去执行 Microtask 的消息队列,再去执行 Timer 的消息队列。

大致的流程如下所示:

Dart 的同步与异步

下面用这个简单的例子验证下:

void main(List<String> arguments) async{
  print('main start!');
  //微任务
  scheduleMicrotask((){
    print('Microtask 执行!第一次');
  });
  //事件任务
  Timer.run(() {
    print('Timer 执行!第一次');
    Timer.run(() {
      print('Timer 执行!第二次');
    });
    scheduleMicrotask((){
      print('Microtask 执行!第二次');
    });
  });
  print('main end!');
}

输出结果:

main start!
main end!
Microtask 执行!第一次
Timer 执行!第一次
Microtask 执行!第二次
Timer 执行!第二次

Process finished with exit code 0

有两点要注意:

  • 当在 Timer 中执行 scheduleMicrotaskTimer.run 的时候,会往相应的队列添加数据,直到 Microtask QueueEvent Queue 都为空时,才会退出程序执行。
  • 正常情况下,我们使用 Timer.run 即可,尽量不要使用 scheduleMicrotask,因为 Microtask 在 flutter 中会承载触摸事件等优先级较高的事件处理。

Timer 具体的存储位置在:sdk > lib > _internal > vm > lib > timer_impl.dart > _Timer_impl 中:

class _TimerHeap {
  List<_Timer> _list;
  int _used = 0;

  _TimerHeap([int initSize = 7])
      : _list = List<_Timer>.filled(initSize, _Timer._sentinelTimer);
         
  ······
}

Future

我们先来看个栗子🌰:

void main() {
  print('main start');
  Future.delayed(Duration(seconds: 3), (){
    print('Future run');
  });
  print('main end');
}
main start
main end
Future run

Process finished with exit code 0

我们可以看到 Future 相比于 main() 确实延期执行了,就像开启了一个线程一样,那我们该如何去理解 Future?

这,我们可以从源码方面进行分析:

  factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
    if (computation == null && !typeAcceptsNull<T>()) {
      throw ArgumentError.value(
          null, "computation", "The type parameter is not nullable");
    }
    _Future<T> result = new _Future<T>();
    new Timer(duration, () {
      if (computation == null) {
        result._complete(null as T);
      } else {
        try {
          result._complete(computation());
        } catch (e, s) {
          _completeWithErrorCallback(result, e, s);
        }
      }
    });
    return result;
  }

很简单,其实它只是包装成了 Timer,所以,并没有开启一个新线程,而是按照了事件循环机制,放到 Event Queue中,优先执行了 main()

那我们又该如何去理解 await 和 async ?

同样,我们再看看一个栗子🌰:

void main() async{
  print('main start');
  final data = await getNetworkData();
  print('main end. network data: $data');
}

Future<String> getNetworkData(){
  return Future.delayed(Duration(seconds: 3), () => 'I am robot');
}
main start
main end. network data: I am robot

Process finished with exit code 0

我们看日志输出效果,感觉 await 把 main() 阻塞了,只有成功获取得到 Future 的值才会继续执行下去。

但是,其实这里并没有进行阻塞,而是使用了 select 的概念,也就是非阻塞式等待。

这里就可能有人有疑问了?这阻塞式和非阻塞式有什么区别?

我们都知道,系统的 CPU 时间片的分配最小单位为线程,而程序的功能只是线程的代码执行而已,阻塞式就是当前线程放弃当前时间片,进入等待状态,交由其它线程执行完,再进行唤醒,再等待时间片进行执行;而非阻塞式则是没有进入等待状态,而是通过自旋的方式进行执行,等待其它任务完成,它再继续执行下去。

自旋的最简单理解就是:

var status = true;
while(status){}

等待更改 status 值从而跳出当前循环。

isolate

isolate 简单可以理解为一个线程,和 Java 的 Thread 相似,但是他们之间却还有很大的一个区别,我们先来看看 Java 的运行数据区:

Dart 的同步与异步

这里有一个很明显的特点,就是线程具有共享的区域,特别是堆,说明线程之间的共享只要传堆的引用即可,无需真正拷贝数据过去,但是,这样就会出现一个问题,那便是共享的数据可能出现不安全的情况,即多线程能够同时修改同一份数据,由此,Java 延伸出锁的概念,Synchronized、ReentrantLock 等等便孕育而生,就是为了解决多线程并发问题。

Dart 为了避免这种情况,使用了另外一种概念:

Dart 的同步与异步

也就是 isolate 之间尽可能不保持联系,他们之间的数据传输都是通过 port 传输真实的数据,而不是传对象的引用。

同时由于 isolate 的相对独立性,所以 Dart 不需要锁的概念,并且在内存回收上,无需 STW(Stop the world),直接回收 isolate 中全部资源即可。同样的,我们也来看看一个栗子🌰:

void main() async{
  print('main start');
  ReceivePort receivePort = ReceivePort();
  Isolate.spawn(getNetworkData,["getUserInfo",receivePort.sendPort]);
  //监听接收消息
  receivePort.listen((message) {
    print("收到消息:$message");
  });
  print('main end');
}

void getNetworkData(var message){
  // 获取传入的数据
  String path = message[0];
  SendPort sendPort = message[1];

  sendPort.send('path : $path, info : UserInfo');
}

输出的结果为:

main start
main end
收到消息:path : getUserInfo, info : UserInfo

我们可以看出,isolate 是通过 ReceivePort 和 SendPort 来进行数据的发送和接收。

另外,有一点需要注意,就是该程序运行后,并没有退出,ReceivePort 仍在等待消息,所以,我们需要适时将其关闭:

receivePort.listen((message) {
  print("收到消息:$message");
  receivePort.close();
});