´--- theme: channing-cyan
EventLoop
首先应该注意,Dart是单线程语言
Dart中的事件循环(Event Loop)
Dart 是一种单线程编程语言。如果某个操作阻塞了 Dart 线程,则应用程序在该操作完成之前没有任何进展。因此,为了可扩展性,没有 I/O 操作阻塞是至关重要的。dart:io 不是阻塞 I/O 操作,而是使用受 node.js、 EventMachine和 Twisted 启发的异步编程模型。
以上内容引用自这里
一般情况下实现异步的方式是多线程,另一种就是异步编程模型(Event Loop)
Event Loop
以下面这串代码为例,看下是如何实现异步的
void main() {
print('before getIP');
getIP().then((value) {
print(value);
});
print('after getIP');
}
复制代码
getIP()
返回的是一个Future
,用于获取本机ip地址的网络请求; 首先执行print('before getIP');
语句,等到getIP()
时,dart首先会将getIP()
方法返回的Future记录在一个内部数组中(此时Future的状态=incomplete),接着会把getIP()
内部执行的代码推入事件队列,并且执行下一步,也就是print('after getIP');
;然后检查事件队列的任务继续执行,当 getIP()
执行完毕后会通过注册的回调,也就是then
返回value,所以耗时事件不会阻塞循环,而在耗时事件之后的事件也就有机会被执行。
注意: 这里针对的是等待任务(比如I/O、网络请求),如果是计算密集型任务则应当尽可能利用处理器的多核,实现并行计算。
Event Loop中的两个队列
上面提到的事件队列指的是:Event Queue
和 Microtask Queue
;
- Event Queue
- 外部事件:
- I/O
- 手势
- 绘图
- 计时器
- Steam
- ...
- 内部事件:
- Future
- 外部事件:
- Microtask Queue
- 用于非常简短且需要异步执行的内部动作
不过大多数情况我们都不会用到Microtask Queue
,该队列是交给Dart自己处理的。 而Microtask Queue
优先级大于Event Queue
添加事件到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.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 实例 返回一个状态(= incomplete);
- 如果存在下一个同步代码,执行它(非 Future 的执行代码)
只要事件循环从 Event Loop 中获取它,被 Future 引用的代码将像其他任何 Event 一样执行。
当该代码将被执行并将完成(或失败)时,then() 或 catchError() 方法将直接被触发。 需要记住一些非常重要的事情:
Future 并非并行执行,而是遵循事件循环处理事件的顺序规则执行。
Async
当你使用Async关键字作为方法生命的后缀时,Dart会将其理解为:
- 该方法返回值是一个Future
- 它同步执行该方法直到第一个await关键字,然后它暂停该方法其他部分的执行
- 一旦由await关键字引用的Future执行完成,下一行代码将立即执行
了解这一点是非常重要的,因为很多开发者认为 await 暂停了整个流程直到它执行完成,但事实并非如此。他们忘记了Event Loop的运作模式……
注意:await在同一个Future下生效。比如⬇️
//生效,await后面的代码会等待
void test() async {
methodA();
await methodB();
await methodC();
}
//三个await没有关系,因为没有在同一个Future,
Future(() async {
await Future.delayed(Duration(seconds: 1));
print('A');
});
Future(() async {
await Future.delayed(Duration(seconds: 1));
print('B');
});
Future(() async {
await Future.delayed(Duration(seconds: 1));
print('C');
});
复制代码
另外,也需要谨记
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:所有的内容都运行在一个相同的代码「块」中,因此能够一行一行按照顺序执行。
Isolate
Dart是单线程语言,难道说我们是无法使用并发编程的吗, 答案当然是否定的,
Dart是单线程语言,但是可以使用Isolate实现并发编程, 对于Isolate
的官方解释是:
independent workers that are similar to threads but don't share memory(类似于线程但不共享内存的独立工作者)
这里并没有把Isolate
直接叫做线程,而是用类似于线程表示。
但是我们可以这样理解,
在Dart中每个线程是被封装在Isolate中,各线程间不共享内存,避免了dead lock的问题,由于线程独立,垃圾回收机制也非常高效。不同Isolate之间通过消息进行通信。
关系如下图
每个Isolate
都有自己Event Loop
(事件循环)
每个「Isolate」都拥有自己的「事件循环」及队列(MicroTask 和 Event)。这意味着在一个 Isolate 中运行的代码与另外一个 Isolate 不存在任何关联。
启动一个Isolate
1.用底层实现
需要自己建立通信,自己管理新建的Isolate和生命周期,自由度较高,但使用相对麻烦
由于Isolate之间不共享内存,因此,我们需要找到一种方法在调用者isolate与被被调用者isolate之间建立通信。
每个 Isolate 都暴露了一个将消息传递给另一个Isolate 的被称为SendPort的端口。两个isolate都需要互相知道对方的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;
}
复制代码
2.一次性计算(compute)
直接传入方法和参数即可,内部自己管理Isolate,会在方法直接完成后直接释放Isolate,使用相对简单
- 产生一个 Isolate,
- 在该 isolate 上运行一个回调函数,并传递一些数据,
- 返回回调函数的处理结果,
- 回调执行后终止 Isolate。
特别注意
Platform-Channel 通信仅仅由主 isolate 支持。该主 isolate 对应于应用启动时创建的 isolate。
也就是说,通过编程创建的 isolate 实例,无法实现 Platform-Channel 通信, 还有另一个解决办法 ->链接