单线程异步
一般来说,dart是单线程的,通常我们的flutter代码都是运行在一个线程里,并无主次线程之分,除非自己新开了一个isolate,否则线程不会切换。但是同一个线程里dart却能实现异步编程,那么它的异步是怎么实现的呢?
事件循环
dart的主线程执行的是同步任务,但它内部维护了一个事件循环(Event Loop)和两个任务队列(Event queue和Microtask queue),它们负责执行异步任务。
Event queue:io、timer、绘制事件等。
Microtask queue:加入微任务中的事件。优先级最高
执行流程
image.png优先级顺序依次为:同步任务 > Microtask queue > Event queue,即每次同步任务执行完毕后,eventLooper会先轮询检查微任务队列,按照先进先出的顺序一次执行微任务队列,当微任务队列当中的任务执行完毕之后再轮询检查事件队列,依然按照先进先出的顺序依次执行事件队列当中的任务。
例如:
main(){
print('main1');
Timer.run(() { print('timer1'); });
Timer.run(() { print('timer2'); });
print('main2');
}
执行结果为:
main1
main2
timer1
timer2
虚拟机调用机制
和Java类似,dart虚拟机在运行dart程序时,会涉及到以下几种数据结构:
1.stack 方法调用栈,用于方法调用
2.heap 堆,用于存储对象,可主动销毁
3.Queue 任务队列,用于存储异步任务
4.Event Dispatcher 它会将队列中的任务依次取出然后同步执行。
示例2:
print('main1');
Timer.run(() {
scheduleMicrotask(() {
print('microtask1');
});
scheduleMicrotask(() {
print('microtask2');
});
});
scheduleMicrotask(() {
print('microtask3');
Timer.run(() {
print('timer 1');
});
});
Timer.run(() {
print('timer2');
});
print('main2');
该函数依次执行的顺序如下:
main1
main2
microtask1
microtask2
microtask3
timer2
timer1
为何会是这种结果呢?从数据流转的顺序看,我们一步步的分析,打印main1 -> microtask1()方法加入微任务队列 -> microtask2加入微任务队列 -> microtask3加入微任务队列 -> timer2()加入事件队列 ->打印main2,至此,同步任务都执行完了,接下来执行异步任务,异步任务是先轮训微任务队列,因此顺序是:打印microtask1 -> 打印microtask2 -> 打印microtask3-> time1()加入事件队列;再执行事件队列任务:打印timer2 -> 打印timer1。
为何单线程可以异步
因为主线程多数时候都是处于等待状态,是比较空闲的,等待用户交互、网络请求结果或者io操作结果。
而这个等待的过程并不是阻塞的,一个线程里任何任务都可以拆分成最基本的操作命令,这些命令有些是计算或者存储,有些是缓存和总线等内存读取工作,它们分属不同的元器件来执行,CPU的执行效率通常比较高,而io读写等操作比较耗时,因此当CPU执行当前任务到需要等待数据输入时会把当前任务挂起,继续执行下一条任务,而当数据读取完成后又继续执行挂起的任务。这样就可以大大提高CPU的利用效率。
这样设计的好处就是在提高资源利用效率的同时,避免了多线程的死锁以及资源频繁切换问题。
Future
dart的异步是通过Future函数来实现的,Future顾名思义,就是未来、期货的意思,不会马上执行,代表异步任务。其内部实现实际就是一个Timer,将事件推入事件队列当中去处理。
- 这里提一个Timer的延时与否的执行流程差别:
如果Timer是非延时的,那么会马上发送一个_ZERO_EVENT的消息直接交给事件队列。
如果Timer是延时的,则会将timer加入到一个二叉堆中,根据唤醒时间将堆中timer进行排序。有一个event handler专门来处理计时任务,它和timer保持通信,当有timer需要唤醒时event handler会发送一个_TIMEOUT_EVENT的消息,timer收到后再调用event handler的_sendData方法将其交给事件队列。*
Future函数通常返回的也是Future,因此它后面可以链式的调用无数个then。
- 每遇到一个Future都会将其加入EventQueue
- 外部函数同步执行
- 每个then都会在上一个Future执行完毕后同步执行,如果then后面是Future,则继续加入EventQueue
- EventLooper依次取出event,同步执行event
再看一个实例:
void test3() {
Future(() {
print('f1');
});
Future(() {
print('f2');
}).then((value) {
print('f3');
scheduleMicrotask(() {
print('f4');
});
}).then((value) => print('f5'));
Future(() {
print('f6');
}).then((value) {
Future(() {
print('f7');
}).then((value) => print('f8'));
});
Future(() {
print('f9');
});
scheduleMicrotask(() {
print('f10');
});
print('f11');
}
这个实例分析就不一一解释了,最后的输出结果是:
f11
f10
f1
f2
f3
f5
f4
f6
f9
f7
f8
多线程
Isolate
有了单线程异步,是不是就已经足够了呢?并没有,当遇到一些CPU密集型的任务时,单线程并不能最大效率的利用计算机资源,多核资源会闲置。因此如果需要执行一些并发任务就需要充分利用计算机的多核资源,为此dart设计了isloate多线程模式。
dart多线程是由isolate来实现的。
isolate,字面意思是隔离,因此可以看出它和我们一般意义上的线程Thread是不一样的。
- 线程隔离:和Thread相比在于内存隔离,即它拥有自己独立的内存和资源,也就是在执行多线程任务时它会把其他线程传递过来的资源拷贝一份自己用,和其他isolate并不资源共享。这样可以较大效率的利用硬件性能和减少线程资源交互所带来的开销。
- 每个isolate都有自己独立的EventLooper和Queue
- 消息通信使用port端口
单向通信
Isloate的创建通常使用Isolate.spawn方法,它可以接受两个参数,第一个是新Isolate内部执行的方法,第二个是传入新Isloate的参数。
dart提供了一个ReceivePort用来进行Isolate通信,ReceivePort提供了一个sendPort,我们可以将这个sendPort传入新isolate,这样就可以在第一个方法里面利用sendPort发送消息。
最后利用ReceivePort的listen方法监听新Isolate传过来的消息。
实例如下:
void test6() async {
print('current ='+Isolate.current.debugName.toString());
ReceivePort receivePort = ReceivePort();
Isolate isolate = await Isolate.spawn(newThread, receivePort.sendPort);
receivePort.listen((message) {
print('收到新isolate消息:'+message);
receivePort.close();
isolate.kill();
});
}
执行结果
current =main
收到新isolate消息:send Msg from newIsolate =newThread
这个是单向通信的实例,具体流转如下图:
image.png
双向通信
同样的,Isolate也支持双向通信,道理和单向通信是相通的。都是在自己的Isolate里创建一个ReceivePort,将它的sendPort传入另外一个Isolate。比如两个Isolate,Isolate1将自己的sendPort1传入Isolate2,isolate2就可以通过sendPort1向isolate1发送消息;而isolate1怎么向isolate2发消息呢?在isolate2里面也可以创建一个ReceivePort2,通过sendPort1将它的sendPort2发送给isolate1,这样isolate1就拥有了sendPort2,就可以向sendPort2发送消息了。
流转图如下:
image.png
网友评论