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 Queue 和 Microtask Queue;
- Event Queue
- 外部事件:
- I/O
- 手势
- 绘图
- 计时器
- Steam
- ...
- 内部事件:
- Future
- 外部事件:
- Microtask Queue
- 用于非常简短且需要异步执行的内部动作(比如Stream的异步通知)
不过大多数情况我们都不会用到Microtask Queue,该队列是交给Dart自己处理的。
而Microtask Queue优先级大于Event Queue
Event Loop的执行过程
如何添加任务到Microtask Queue和Event 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 loop | 1. before loop |
| 2. end of loop | 2. 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没有,这是最为关键的区别。
关系如下图
每个Isolate都有自己Event Loop(事件循环)
每个「Isolate」都拥有自己的「事件循环」及队列(MicroTask 和 Event)。这意味着在一个 Isolate 中运行的代码与另外一个 Isolate 不存在任何关联。
启动一个Isolate
1.用底层实现
需要自己建立通信,自己管理新建的Isolate和生命周期,自由度较高,但使用相对麻烦
由于Isolate之间不共享内存,因此,我们需要找到一种方法在调用者isolate与被被调用者isolate之间建立通信。
每个 Isolate 都暴露了一个将消息传递给另一个Isolate 的被称为SendPort的端口。两个isolate都需要互相知道对方的sendPort才可以通信
如下代码例子是一个双向传递的过程,因此双方需要互相知道sendPort
- callerReceivePort.sendPort:调用者的端口
- newIsolateSendPort.sendPort:被调用者的端口
- 创建Isolate并建立通信
- 发送消息
- 销毁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,使用相对简单
- 产生一个 Isolate,
- 在该 isolate 上运行一个回调函数,并传递一些数据,
- 返回回调函数的处理结果,
- 回调执行后终止 Isolate。
特别注意
Platform-Channel 通信仅仅由主 isolate 支持。该主 isolate 对应于应用启动时创建的 isolate。
也就是说,通过编程创建的 isolate 实例,无法实现 Platform-Channel 通信, 还有另一个解决办法 ->链接