Dart 异步与多线程
Dart单线程
Dart
中的事件循环是单线程的,在流畅性与安全性体验较好,核心分为主线程、微任务、宏任务。主线程主要包括业务处理、网络IO、本地文件IO、异步等事件。dart的单线程中有两个事件队列,一个是微任务队列、一个是事件队列。
- 微任务队列
微任务队列包含有 Dart 内部的微任务,主要是通过 scheduleMicrotask 来调度。
- 事件队列
事件队列包含外部事件,例如 I/O 、 Timer ,绘制事件等等。
事件循环 Event loop
链式指定事件顺序
如果你的代码具有依赖型,最好是显式的。这有助于开发人员理解您的代码并使您的代码更加健壮。
想给事件队列中链式添加任务的错误操作示范:
future.then(...set an important variable...);
Timer.run(() {...use the important variable...});
正确的是:
future.then(...set an important variable...)
.then((_) {...use the important variable...});
在Dart
单线程中,他们的结果都是一致的,但是理解起来难易程度不是一致的。
如何创建任务
当你想稍后在任务中执行你的代码,可以使用Future
类,它将把任务添加到事件队列末尾,或者使用顶级函数scheduleMicrotask
把任务添加到微任务队列的末尾。
为了更好的使用then
,或者在发生错误时也需要执行的话,那么请在whenComplate()
代替then
.
如何添加任务到事件队列
可以使用Timer
或者Future
都可以
您也可以使用Timer计划任务,但是如果任务中发生任何未捕获的异常,则应用程序将退出。相反,我们建议使用Future,它建立在Timer之上,并增加了诸如检测任务完成和对错误进行响应的功能。
添加代码到事件队列
new Future(() {
// ...code goes here...
});
你可以使用then
或者whenComplate
执行后边的任务,看下这个例子
new Future(() => 21)
.then((v) => v*2)
.then((v) => print(v));
如果你想稍后再执行的话,请使用Future.delayed()
:
new Future.delayed(const Duration(seconds:1), () {
// ...code goes here...
});
了解了基本的用法之后,那么如何把他们搭配使用呢?
使用任务
有了单线程和队列,那必然有对应的循环,这样子才能执行不同的队列任务和处理事件,那么我们看下 循环。
- 进入
main
函数,并产生相应的微任务和事件队列 - 判断是否存在微任务,有则执行,没有则继续。执行完判断是否还有微任务,有则执行,没则继续
- 如果不存在可执行的微任务,则判断 是否有事件任务,有则执行,无则继续返回判断是否存在事件任务
- 在微任务和事件任务同样可以产生新的 微任务和事件任务,所以需要再次判断是否存在新的微任务和事件任务。

验证一下上边的运行原理,我们看下下边的代码:
void main() {
test();
}
/// 微任务
/// 定时器
void test() async {
print('start');
scheduleMicrotask(() {
print('Microtask 1');
});
Future.delayed(Duration(seconds: 0)).then((value) {
print('Future 1');
});
Timer.run(() {
print('Timer 1');
});
print('end');
}
运行过程如下:
- 首先启动
main
函数,打印start
- 执行
scheduleMicrotask
微任务,添加任务到微任务队列中 - 执行
Future
事件,给事件队列添加任务 - 执行
timer
事件,给事件队列添加任务 - 执行事件打印
end
- 第二次循环判断是否有微任务,刚才已添加微任务,现在执行微任务,打印
Microtask 1
- 判断是否有事件任务,刚才已添加
Future
任务,执行打印Future 1
- 判断是否有事件任务,刚才已添加
Timer 1
任务,执行打印Timer1
输出
start
end
Microtask 1
Future 1
Timer 1
看下面这个例子证明Timer
和Future
是一个类型的事件。
/// 微任务
/// 定时器
void test2() async {
print('start');
scheduleMicrotask(() {
print('Microtask 1');
});
Timer.run(() {
print('Timer 1');
});
Future.delayed(Duration(seconds: 0)).then((value) {
print('Future 1');
});
print('end');
}
输出
start
end
Microtask 1
Timer 1
Future 1
上面的test
和test2
中Timer
和Future
位置调换了一下,也就是添加事件任务先后顺序颠倒了一下,在执行的时候也颠倒了一下。我们再看Future
源码:
factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
_Future<T> result = new _Future<T>();
new Timer(duration, () {
if (computation == null) {
result._complete(null);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
Future.delayed
源码本质就是将任务添加到Timer
中,在指定时间后运行该任务。
执行完事件需要再次判断是否有微任务
微任务队列优先级高于事件队列,所以每次执行任务首先判断是否存在未执行的微任务。
事件队列执行完,有可能有新的微任务被添加到队列中,所以还需要扫描微任务队列一次。 看下面的例子:
void test3() async {
print('start'); //1
scheduleMicrotask(() {//2
print('Microtask 1');//5
});
Timer.run(() {//3
print('Timer 1');//6
Timer.run(() {//9
print('Timer 1 Microtask 2 ');//10
});
scheduleMicrotask(() {//7
print('Microtask 2');//8
});
});
print('end');//4
}
执行顺序:
- 打印
start
- 添加微任务到队列中
- 添加
Timer 1
到事件队列 - 执行打印
end
任务 - 判断是否有微任务,发现微任务
Microtask 1
,立即执行 - 判断是有有微任务,发现没有,则判断是否有事件任务,发现
Timer 1
任务并执行,添加事件任务Timer 1 Microtask 2
,添加微任务Microtask 2
到微任务队列 - 判断是否有微任务,发现微任务
Microtask 2
,并执行。 - 判断是否有事件任务,发现事件任务
Timer 1 Microtask 2
并执行。
结果输出
start
end
Microtask 1
Timer 1
Microtask 2
Timer 1 Microtask 2
微任务或者事件任务会卡吗?
根据前边的了解,执行完微任务再执行事件任务,当某个事件处理时间需要很长,则后边的任务则会一直处于等待状态。下边我们看一个例子,当任务足够都,还是需要一定时间去处理的。
void test4() async {
print('start ${DateTime.now()}');
for(int i =0;i < 99999;i++){
scheduleMicrotask(() {
print('Microtask 1');
});
}
Timer.run(() {
print('Timer 1 ${DateTime.now()}');
});
print('end ${DateTime.now()}');
}
输出:
start 2020-07-28 17:44:11.561886
end 2020-07-28 17:44:11.593989
...
Microtask 1
.....
Timer 1 2020-07-28 17:44:11.893093
可以看出这些任务执行完成耗时基本达到了0.33
秒。
当一个线程出现处理任务不够了,那么就需要在开启一个线程了。
Isolates 多线程
上边的 dart
进入main函数是单线程的,在Dart中,多线程叫做Isolates
线程,每个Isolates线程不共享内存,通过消息机制通信。
我们看个例子,利用Dart
的Isolates
实现多线程。
void test5()async{
final rece = ReceivePort();
isolate = await Isolate.spawn(sendPort, rece.sendPort);
rece.listen((data){
print('收到了 ${data} ,name:$name');
});
}
void sendPort(SendPort sendPort){
sendPort.send('发送消息');
}
Isolate isolate;
String name='fgyong';
void main() {
test5();
}
输出
收到了 发送消息 ,name:fgyong
多线程相互沟通怎么处理?
创建线程之后子线程需要发送主线程一个端口和消息,主线程记录该端口,下次和子线程通讯使用该端口即可。
具体代码如下:
/// 新线程执行新的任务 并监听
Isolate isolate;
Isolate isolate2;
void createTask() async {
ReceivePort receivePort = ReceivePort();
isolate = await Isolate.spawn(sendP1, receivePort.sendPort);
receivePort.listen((data) {
print(data);
if (data is List) {
SendPort subSencPort = (data as List)[1];
String msg = (data as List)[0];
print('$msg 在主线程收到');
if (msg == 'close') {
receivePort.close();
} else if (msg == 'task') {
taskMain();
}
subSencPort.send(['主线程发出']);
}
});
}
void sendP1(SendPort sendPort) async {
ReceivePort receivePort = new ReceivePort();
receivePort.listen((data) async {
print(data);
if (data is List) {
String msg = (data as List)[0];
print('$msg 在子线程收到');
if (msg == 'close') {
receivePort.close();
} else if (msg == 'task') {
var m = await task();
sendPort.send(['$m', receivePort.sendPort]);
}
}
});
sendPort.send(['子线程线程发出', receivePort.sendPort]);
}
Future<String> task() async {
print('子线程执行task');
for (var i = 0; i < 99999999; i++) {}
return 'task 完成';
}
void taskMain() {
print('主线程执行task');
}
输出:
[子线程线程发出, SendPort]
子线程线程发出 在主线程收到
[主线程发出]
主线程发出 在子线程收到
更多子线程与主线程交互请上代码库查看
复杂问题解决方案
假设一个项目,需要 2 个团队去完成,团队中包含多项任务。可以分为 2 个高优先级任务(高优先级的其中,会产生2个任务,一个是紧急一个是不紧急),和 2 个非高优先级任务(非高优先级的其中,会产生有 2 个任务,一个是紧急一个是不紧急)。其中还有一个是必须依赖其他团队去做的,因为本团队没有那方面的资源,第三方也会产生一个高优先级任务和一个低优先级任务。
根据紧急任务作为微任务,非紧急任务作为事件任务来安排,第三方是新开线程
主任务 | 高优先级(微任务) | 低优先级(事件任务) | 第三方(Isolate) |
---|---|---|---|
H1 | H1-1 | L1-2 | 否 |
H2 | H2-1 | L2-2 | 否 |
L3 | H3-1 | L3-2 | 否 |
L4 | H4-1 | L4-2 | 否 |
I5 | IH5-1 | I5-2 | 是 |
void test6() {
createTask();//创建线程
scheduleMicrotask(() {//第一个微任务
print('H1');
scheduleMicrotask(() {//第一个紧急任务
print('H1-1');
});
Timer.run(() {//第一个非紧急任务
print('L1-2');
});
});
scheduleMicrotask(() {// 第二个高优先级任务
print('H2');
scheduleMicrotask(() {//第二个紧急任务
print('H2-1');
});
Timer.run(() {//第二个非紧急任务
print('L2-2');
});
});
Timer.run(() {// 第一个低优先级任务
print('L3');
scheduleMicrotask(() {//第三个紧急任务
print('H3-1');
});
Timer.run(() {//第三个非紧急任务
print('L3-2');
});
});
Timer.run(() {// 第二个低优先级任务
print('L4');
scheduleMicrotask(() {//第四个紧急任务
print('H4-1');
});
Timer.run(() {//第四个非紧急任务
print('L4-2');
});
});
}
/// 新线程执行新的任务 并监听
Isolate isolate;
void createTask() async {
ReceivePort receivePort = ReceivePort();
isolate = await Isolate.spawn(sendPort, receivePort.sendPort);
receivePort.listen((data) {
print(data);
});
}
/// 新线程执行任务
void sendPort(SendPort sendPort) {
scheduleMicrotask(() {
print('IH5-1');
});
Timer.run(() {
print('IL5-2');
});
sendPort.send('第三方执行任务结束');
}
运行结果
H1
H2
H1-1
H2-1
L3
H3-1
L4
H4-1
L1-2
L2-2
L3-2
L4-2
IH5-1
IL5-2
第三方执行任务结束
可以看到H
开头的为高优先级,L
开头为低优先级,基本高优先级运行都在低优先级之前,符合预期。
但是第三方的为什么在最后才执行了?
由于创建线程需要事件,其他任务均为耗时太短,那么我们重新做一个耗时事件长的任务即可。
createTask();//创建线程
for (var i = 0; i < 9999999999; i++) {
}
...
输出:
IH5-1
IL5-2
H1
H2
H1-1
H2-1
第三方执行任务结束
L3
H3-1
L4
H4-1
L1-2
L2-2
L3-2
L4-2
为什么 第三方执行任务结束 在正中间?而不是在H1上边??
因为这个事件属于低优先级,而H
开头的都是高优先级任务。
为什么 第三方执行任务结束 ??
在L3
上边。而不是在最下边,一定在低优先级队列的第一个吗 ?
由于开始的耗时操作事件太长,导致所有任务执行前,第三方任务已经执行完成,所以 第三方执行任务结束 是第一个添加到低优先级任务队列的,所以在低优先级队列第一个执行。
当耗时操作比较少时,则 第三方执行任务结束 添加顺序则不确定。
总结
Dart
中异步和多线程是分开的,异步只是事件循环中的多事件轮训的结果,而多线程可以理解为真正的并发(多个线程同时做事)。在单个线程中,又分为微任务和其他事件队列,微任务队列优先级高于其他事件队列。
参考
- Flutter快用快学
- The Event Loop and Dart
- 代码库 查看demo
转载自:https://juejin.cn/post/6855129006103576584