Flutter异步编程

单线程异步

一般来说,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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容