Dart中的异步编程和并发编程

1,473 阅读5分钟

Dart中的异步编程模型——Event Loop

首先应该注意,Dart是单线程语言

Event Loop

Dart 是一种单线程编程语言。如果某个操作阻塞了 Dart 线程,则应用程序在该操作完成之前没有任何进展。因此,为了可扩展性,没有 I/O 操作阻塞是至关重要的。dart:io 不是阻塞 I/O 操作,而是使用受 node.js、 EventMachine和 Twisted 启发的异步编程模型。
以上内容引用自这里

一般情况下实现异步的方式是多线程,另一种就是异步编程模型(Event Loop)

Event Loop

和大多数单线程语言一样,Dart使用了事件循环(Event Loop)实现了异步编程, 事件循环机制很适合等待型耗时任务,而如果是计算密集型任务则应当使用并发编程

Event Loop中的两个队列

一个事件循环包含两个事件队列,分别是是:Event QueueMicrotask Queue

  • Event Queue
    • 外部事件:
      • I/O
      • 手势
      • 绘图
      • 计时器
      • Steam
      • ...
    • 内部事件:
      • Future
  • Microtask Queue
    • 用于非常简短且需要异步执行的内部动作(比如Stream的异步通知)

不过大多数情况我们都不会用到Microtask Queue,该队列是交给Dart自己处理的。 而Microtask Queue优先级大于Event Queue

Event Loop的执行过程

image.png

如何添加任务到Microtask QueueEvent Queue

  • 可以直接运行(没有添加到任何队列)
    • Future.sync()
    • Future.value()
    • _.then()
  • Microtask Queue
    • scheduleMicrotask()
    • Future.microtask()
    • _complete.then()
  • Event Queue
    • Future()
    • Future.delayed()
    • Timer()
特别强调:

_.then():表示在没有完成的Future中使用.then,这种情况下不会添加到任何队列,而是在Future执行完毕后立即执行

_complete.then():表示在已经完成的Future中使用.then,会将添加到Microtask Queue中(因为当前future已经完成,所以需要尽快的将它的then执行,因此要加入到优先级较高的队列中)

Future.delayed():表示在延迟Duration后添加到Event Queue

Future.delayed(Duration(seconds: 0),())也会添加到Event Queue

Timer():Future内部的实现正是Timer

其实关于.then()的解释,源码注释中已经给了足够的说明

If this future is already completed, the callback will not be called,immediately, but will be scheduled in a later microtask.

测试_complete.then()加入Microtask Queue的情况
void main() {
  scheduleMicrotask(() => print('Microtask 1'));
  Future.microtask(() => print('Microtask 2'));

  //由于Future.value()已经执行完成,所以.then也应该尽快完成,因此需要加入到优先级高到Microtask Queue中
  Future.value(1).then((value) => print('Microtask 3'));
  print('main 1');
}

打印结果⬇:

flutter: main 1
flutter: Microtask 1
flutter: Microtask 2
flutter: Microtask 3
测试_.then()立即执行的情况
void main() {
  //如果.then加入任务队列,打印顺序为delayed -> then 1 -> Microtask -> then 2
  //如果.then不加入任务队列,则需要立即执行,打印顺序为delayed -> then 1 -> then 2 -> Microtask
  Future.delayed(Duration(seconds: 1),() => print('delayed'))
  .then((value) {
    scheduleMicrotask(() => print('Microtask'));
    print('then 1');
  })
  .then((value) => print('then 2'));
  print('main 1');
}

打印结果⬇:

flutter: main 1
flutter: delayed
flutter: then 1
flutter: then 2
flutter: Microtask

因此结论正确

Future

Future是一个异步执行并且在未来的某一个时刻完成(或失败)的任务

当你实例化一个Future时:

  • Future的一个实例被创建并记录在由 Dart 管理的内部数组中;
  • 需要由此 Future 执行的代码直接推送到 Event 队列中去;
  • future 实例返回一个状态(= _stateIncomplete);
  • 如果存在下一个同步代码,执行它(非 Future 的执行代码

只要Event Loop从队列中获取它,被 Future 引用的代码将像其他任何 Event 一样执行。 当该代码将被执行并将完成(或失败)时,then()  或 catchError()  方法将直接被触发。

需要记住一些非常重要的事情:

Future 并非并行执行,而是遵循事件循环处理事件的顺序规则执行。

Future实例的六种状态:

 //初始未完成状态,等待一个结果
 static const int _stateIncomplete = 0
 //当不需要处理错误时设置的标志。
 static const int _stateIgnoreError = 1
 //等待完成状态, 表示Future对象的计算过程仍在执行中,这个时候还没有可以用的result.
 static const int _statePendingComplete = 2
 //链接状态(一般出现于当前Future与其他Future链接在一起时,其他Future的result就变成当前Future的result)
 static const int _stateChained = 4
 //完成带有值的状态(会调用.then)
 static const int _stateValue = 8
 //完成带有异常的状态(会调用.catchError)
 static const int _stateError = 16

Async

当你使用Async关键字作为方法生命的后缀时,Dart会将其理解为:

  • 该方法返回值是一个Future
  • 它同步执行该方法直到第一个await关键字,然后它暂停该方法其他部分的执行
  • 一旦由await关键字引用的Future执行完成,下一行代码将立即执行 了解这一点是非常重要的,因为很多开发者认为 await 暂停了整个流程直到它执行完成,但事实并非如此。他们忘记了Event Loop的运作模式……

另外,也需要谨记

async 并非并行执行,也是遵循事件循环处理事件的顺序规则执行。

执行method1()和method2()结果分别是什么?:

method1() {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  myArray.forEach((value) async {
    await delayedPrint(value);
  });
  print('end of loop');
}

method2() async {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');

  for(int i=0; i<myArray.length; i++) {
    await delayedPrint(myArray[i]);
  }
  print('end of loop');
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print('delayedPrint: $value');
}
method1()method2()
1. before loop1. before loop
2. end of loop2. delayedPrint: a (after 1 second)
3. delayedPrint: a (after 1 second)3. delayedPrint: b (1 second later)
4. delayedPrint: b (directly after)4. delayedPrint: c (1 second later)
5. delayedPrint: c (directly after)5. end of loop (right after)

method1:使用 forEach()  函数来遍历数组。每次迭代时,它都会调用一个被标记为 async(因此是一个 Future)的新回调函数。执行该回调直到遇到 await,而后将剩余的代码推送到 Event 队列。一旦迭代完成,它就会执行下一个语句:“print(‘end of loop’)”。执行完成后,事件循环 将处理已注册的 3 个回调。

method2:所有的内容都运行在一个相同的代码「块」中,因此能够一行一行按照顺序执行。

Dart中的并发编程——Isolate

Dart是单线程语言,难道说我们是无法使用并发编程的吗, 答案当然是否定的,

Dart是单线程语言,但是可以使用Isolate实现并发编程, 对于Isolate的官方解释是:

independent workers that are similar to threads but don't share memory(类似于线程但不共享内存的独立工作者)

这里并没有把Isolate直接叫做线程,而是用类似于线程表示。

但是我们可以这样理解,

在Dart中每个线程是被封装在Isolate中,各线程间不共享内存,避免了dead lock的问题,由于线程独立,垃圾回收机制也非常高效。不同Isolate之间通过消息进行通信。

Isolate和普通线程的区别

我们可以看到isolate神似Thread,但实际上两者有本质的区别。操作系统内的线程之间是可以有共享内存的而isolate没有,这是最为关键的区别。

关系如下图 image.png

每个Isolate都有自己Event Loop(事件循环)

每个「Isolate」都拥有自己的「事件循环」及队列(MicroTask 和 Event)。这意味着在一个 Isolate 中运行的代码与另外一个 Isolate 不存在任何关联。

启动一个Isolate

1.用底层实现

需要自己建立通信,自己管理新建的Isolate和生命周期,自由度较高,但使用相对麻烦

由于Isolate之间不共享内存,因此,我们需要找到一种方法在调用者isolate与被被调用者isolate之间建立通信。

每个 Isolate 都暴露了一个将消息传递给另一个Isolate 的被称为SendPort的端口。两个isolate都需要互相知道对方的sendPort才可以通信

如下代码例子是一个双向传递的过程,因此双方需要互相知道sendPort

  • callerReceivePort.sendPort:调用者的端口
  • newIsolateSendPort.sendPort:被调用者的端口
  1. 创建Isolate并建立通信
  2. 发送消息
  3. 销毁Isolate
void main() async {

  //1.创建Isolate并建立通信
  //本地临时ReceivePort,用于检索新的isolate的SendPort
  ReceivePort callerReceivePort = ReceivePort();
  Isolate newIsolate = await createAndCommunication(callerReceivePort);

  //新的isolate的SendPort
  SendPort newIsolatePort = await callerReceivePort.first;

  //2.发送消息
  int result = await sendMessage(newIsolatePort, 10000000000);
  print(result);

  //3.释放Isolate
  disposeIsolate(newIsolate);
}

Future<Isolate> createAndCommunication(ReceivePort callerReceivePort) async {
  //初始化新的isolate(spawn():第一个参数是新isolate的入口方法,第二个参数是调用者的的SendPort)
  Isolate isolate = await Isolate.spawn(newIsolateEntry, callerReceivePort.sendPort);
  return isolate;
}

sendMessage(SendPort newIsolateSendPort, int num) async {
  //创建一个临时端口来接受回复
  ReceivePort responsePort = ReceivePort();
  //向调用者提供此isolate的SendPort
  newIsolateSendPort.send([responsePort.sendPort, num]);
  //等待回复并返回
  return responsePort.first;
}

void disposeIsolate(Isolate newIsolate) {
  newIsolate.kill(priority: Isolate.immediate);
  newIsolate = null;
}

//新isolate的入口(注意:在这里的内存都属于新建的isolate)
newIsolateEntry(SendPort callerSendPort) async {
  //一个新的SendPort实例,用来接受来自调用者的消息
  ReceivePort newIsolateReceivePort = ReceivePort();

  //向调用者提供此isolate的SendPort(注意:到这里双方通讯建立完成)
  callerSendPort.send(newIsolateReceivePort.sendPort);

  //监听调用者isolate向新的isolate输入的消息,并处理计算返回数据
  //注意:这里是Isolate的主程序,在isolate的处理和计算都在这里进行
  newIsolateReceivePort.listen((message) {
    //处理计算
    SendPort port = message[0];
    int num = message[1];
    int even = countEven(num);

    //发送结果
    port.send(even);
  });
}

//计算偶数个数(模拟计算密集型任务)
countEven(int num) {
  int count = 0;
  for(var i=0;i<=num;i++){
    if(i%2==0){
      count ++;
    }
  }
  return count;
}
Dart2.15及以后

新增Isolate组概念

Isolate 组中的 isolate 共享各种内部数据结构,这些数据结构则表示正在运行的程序。这使得组中的单个 isolate 变得更加轻便。如今,因为不需要初始化程序结构,在现有 isolate 组中启动额外的 isolate 比之前快 100 多倍,并且产生的 isolate 所消耗的内存减少了 10 至 100 倍

在 Dart 2.15 中,工作器 isolate 可以调用 Isolate.exit(),将其结果作为参数传递。然后,Dart 运行时将包含结果的内存数据从工作器 isolate 传递到主 isolate 中,无需复制,且主 isolate 可以在固定时间内接收结果。我们已经在 Flutter 2.8 中更新了 compute() 实用函数,来利用 Isolate.exit()。如果您已经在使用 compute(),那么在升级到 Flutter 2.8 后,您将自动获得这些性能提升。

void main() async {
  
  ReceivePort receivePort = ReceivePort();
  Isolate.spawn(countTaskInBackground, receivePort.sendPort);
  int result = await receivePort.first;
  if (kDebugMode) {
    print('result = $result');
  }
}

Future countTaskInBackground(SendPort sendPort) async {
  int result = await countEven(100000000);
  return Isolate.exit(sendPort, result);
}
Future<int> countEven(int num) async {
  int count = 0;
  for(var i=0;i<=num;i++){
    if(i%2==0){
      count ++;
    }
  }
  return count;
}
2.一次性计算(compute)

直接传入方法和参数即可,内部自己管理Isolate,会在方法直接完成后直接释放Isolate,使用相对简单

  1. 产生一个 Isolate,
  2. 在该 isolate 上运行一个回调函数,并传递一些数据,
  3. 返回回调函数的处理结果,
  4. 回调执行后终止 Isolate。

特别注意

Platform-Channel 通信仅仅主 isolate 支持。该主 isolate 对应于应用启动时创建的 isolate

也就是说,通过编程创建的 isolate 实例,无法实现 Platform-Channel 通信, 还有另一个解决办法 ->链接

参考资料

github.com/xitu/gold-m…

www.bilibili.com/video/BV12K…

juejin.cn/post/706514…

mp.weixin.qq.com/s/03729uUAE…