20年圣誕
2020 神奇的一年众弓,載入史冊(cè)的一年业簿,改變了很多人的命運(yùn)壁却,曼谷第一開(kāi)膛手也轉(zhuǎn)行入坑Flutter批狱,廢話不多說(shuō),干就完了
Event Loop 機(jī)制
像iOS runloop一樣展东,flutter也有事件循環(huán)精耐,不同的是,那就是 Dart 是單線程的琅锻。那單線程意味著什么呢卦停?這意味著 Dart 代碼是有序的,按照在 main 函數(shù)出現(xiàn)的次序一個(gè)接一個(gè)地執(zhí)行恼蓬,不會(huì)被其他代碼中斷惊完。另外,作為支持 Flutter 這個(gè) UI 框架的關(guān)鍵技術(shù)处硬,Dart 當(dāng)然也支持異步小槐。需要注意的是,單線程和異步并不沖突荷辕。
為什么單線程也可以異步凿跳?
比如說(shuō),網(wǎng)絡(luò)請(qǐng)求疮方,Socket 本身提供了 select 模型可以異步查詢控嗜;而文件 IO,操作系統(tǒng)也提供了基于事件的回調(diào)機(jī)制骡显。
基于這些特點(diǎn)疆栏,單線程模型可以在等待的過(guò)程中做別的事情,等真正需要響應(yīng)結(jié)果了惫谤,再去做對(duì)應(yīng)的處理壁顶。因?yàn)榈却^(guò)程并不是阻塞的,所以給我們的感覺(jué)就像是同時(shí)在做多件事情一樣溜歪。但其實(shí)始終只有一個(gè)線程在處理你的事情若专。
等待這個(gè)行為是通過(guò) Event Loop 驅(qū)動(dòng)的。事件隊(duì)列 Event Queue 會(huì)把其他平行世界(比如 Socket)完成的蝴猪,需要主線程響應(yīng)的事件放入其中调衰。像其他語(yǔ)言一樣膊爪,Dart 也有一個(gè)巨大的事件循環(huán),在不斷的輪詢事件隊(duì)列窖式,取出事件(比如,鍵盤事件动壤、I\O 事件萝喘、網(wǎng)絡(luò)事件等),在主線程同步執(zhí)行其回調(diào)函數(shù)琼懊,如下圖所示:
異步任務(wù)
圖 1 的 Event Loop 示意圖只是一個(gè)簡(jiǎn)化版阁簸。在 Dart 中,實(shí)際上有兩個(gè)隊(duì)列哼丈,一個(gè)事件隊(duì)列(Event Queue)启妹,另一個(gè)則是微任務(wù)隊(duì)列(Microtask Queue)。在每一次事件循環(huán)中醉旦,Dart 總是先去第一個(gè)微任務(wù)隊(duì)列中查詢是否有可執(zhí)行的任務(wù)饶米,有的話會(huì)處理完所有的任務(wù)。如果沒(méi)有车胡,才會(huì)處理后續(xù)的事件隊(duì)列的流程檬输。
Event Loop 完整版的流程圖,應(yīng)該如下所示:
首先匈棘,我們看看微任務(wù)隊(duì)列丧慈。微任務(wù)顧名思義,表示一個(gè)短時(shí)間內(nèi)就會(huì)完成的異步任務(wù)主卫。從上面的流程圖可以看到逃默,微任務(wù)隊(duì)列在事件循環(huán)中的優(yōu)先級(jí)是最高的,只要隊(duì)列中還有任務(wù)簇搅,就可以一直霸占著事件循環(huán)完域。
微任務(wù)是由 scheduleMicroTask 建立的。如下所示瘩将,這段代碼會(huì)在下一個(gè)事件循環(huán)中輸出一段字符串:
scheduleMicrotask(() => print('This is a microtask'));
一般的異步任務(wù)通常也很少必須要在事件隊(duì)列前完成筒主,所以也不需要太高的優(yōu)先級(jí),因此我們通常很少會(huì)直接用到微任務(wù)隊(duì)列鸟蟹,就連 Flutter 內(nèi)部乌妙,也只有 7 處用到了而已(比如,手勢(shì)識(shí)別建钥、文本輸入藤韵、滾動(dòng)視圖、保存頁(yè)面效果等需要高優(yōu)執(zhí)行任務(wù)的場(chǎng)景)熊经。
異步任務(wù)我們用的最多的還是優(yōu)先級(jí)更低的 Event Queue泽艘。比如欲险,I/O、繪制匹涮、定時(shí)器這些異步事件天试,都是通過(guò)事件隊(duì)列驅(qū)動(dòng)主線程執(zhí)行的。
Dart 為 Event Queue 的任務(wù)建立提供了一層封裝然低,叫作 Future喜每。從名字上也很容易理解,它表示一個(gè)在未來(lái)時(shí)間才會(huì)完成的任務(wù)雳攘。
把一個(gè)函數(shù)體放入 Future带兜,就完成了從同步任務(wù)到異步任務(wù)的包裝。Future 還提供了鏈?zhǔn)秸{(diào)用的能力吨灭,可以在異步任務(wù)執(zhí)行完畢后依次執(zhí)行鏈路上的其他函數(shù)體刚照。
接下來(lái),我們看一個(gè)具體的代碼示例:分別聲明兩個(gè)異步任務(wù)喧兄,在下一個(gè)事件循環(huán)中輸出一段字符串无畔。其中第二個(gè)任務(wù)執(zhí)行完畢之后,還會(huì)繼續(xù)輸出另外兩段字符串:
Future(() => print('Running in Future 1'));//下一個(gè)事件循環(huán)輸出字符串
Future(() => print(‘Running in Future 2'))
.then((_) => print('and then 1'))
.then((_) => print('and then 2’));//上一個(gè)事件循環(huán)結(jié)束后吠冤,連續(xù)輸出三段字符串
當(dāng)然檩互,這兩個(gè) Future 異步任務(wù)的執(zhí)行優(yōu)先級(jí)比微任務(wù)的優(yōu)先級(jí)要低。
正常情況下咨演,一個(gè) Future 異步任務(wù)的執(zhí)行是相對(duì)簡(jiǎn)單的:在我們聲明一個(gè) Future 時(shí)闸昨,Dart 會(huì)將異步任務(wù)的函數(shù)執(zhí)行體放入事件隊(duì)列,然后立即返回薄风,后續(xù)的代碼繼續(xù)同步執(zhí)行饵较。而當(dāng)同步執(zhí)行的代碼執(zhí)行完畢后,事件隊(duì)列會(huì)按照加入事件隊(duì)列的順序(即聲明順序)遭赂,依次取出事件循诉,最后同步執(zhí)行 Future 的函數(shù)體及后續(xù)的 then。
這意味著撇他,then 與 Future 函數(shù)體共用一個(gè)事件循環(huán)茄猫。而如果 Future 有多個(gè) then,它們也會(huì)按照鏈?zhǔn)秸{(diào)用的先后順序同步執(zhí)行困肩,同樣也會(huì)共用一個(gè)事件循環(huán)划纽。
如果 Future 執(zhí)行體已經(jīng)執(zhí)行完畢了,但你又拿著這個(gè) Future 的引用锌畸,往里面加了一個(gè) then 方法體勇劣,這時(shí) Dart 會(huì)如何處理呢?面對(duì)這種情況,Dart 會(huì)將后續(xù)加入的 then 方法體放入微任務(wù)隊(duì)列比默,盡快執(zhí)行幻捏。
下面的代碼演示了 Future 的執(zhí)行規(guī)則,即命咐,先加入事件隊(duì)列篡九,或者先聲明的任務(wù)先執(zhí)行;then 在 Future 結(jié)束后立即執(zhí)行醋奠。
//f1比f(wàn)2先執(zhí)行
Future(() => print('f1'));
Future(() => print('f2'));
//f3執(zhí)行后會(huì)立刻同步執(zhí)行then 3
Future(() => print('f3')).then((_) => print('then 3'));
//then 4會(huì)加入微任務(wù)隊(duì)列榛臼,盡快執(zhí)行
Future(() => null).then((_) => print('then 4'));
- 在第一個(gè)例子中,由于 f1 比 f2 先聲明钝域,因此會(huì)被先加入事件隊(duì)列讽坏,所以 f1 比 f2 先執(zhí)行锭魔;
- 在第二個(gè)例子中例证,由于 Future 函數(shù)體與 then 共用一個(gè)事件循環(huán),因此 f3 執(zhí)行后會(huì)立刻同步執(zhí)行 then 3迷捧;
- 最后一個(gè)例子中织咧,F(xiàn)uture 函數(shù)體是 null,這意味著它不需要也沒(méi)有事件循環(huán)漠秋,因此后續(xù)的 then 也無(wú)法與它共享笙蒙。在這種場(chǎng)景下,Dart 會(huì)把后續(xù)的 then 放入微任務(wù)隊(duì)列庆锦,在下一次事件循環(huán)中執(zhí)行捅位。
通過(guò)一個(gè)綜合案例,來(lái)把之前介紹的各個(gè)執(zhí)行規(guī)則都串起來(lái)搂抒,再集中學(xué)習(xí)一下艇搀。
在下面的例子中,我們依次聲明了若干個(gè)異步任務(wù) Future求晶,以及微任務(wù)焰雕。在其中的一些 Future 內(nèi)部,我們又內(nèi)嵌了 Future 與 microtask 的聲明:
Future(() => print('f1'));//聲明一個(gè)匿名Future
Future fx = Future(() => null);//聲明Future fx芳杏,其執(zhí)行體為null
//聲明一個(gè)匿名Future矩屁,并注冊(cè)了兩個(gè)then。在第一個(gè)then回調(diào)里啟動(dòng)了一個(gè)微任務(wù)
Future(() => print('f2')).then((_) {
print('f3');
scheduleMicrotask(() => print('f4'));
}).then((_) => print('f5'));
//聲明了一個(gè)匿名Future爵赵,并注冊(cè)了兩個(gè)then吝秕。第一個(gè)then是一個(gè)Future
Future(() => print('f6'))
.then((_) => Future(() => print('f7')))
.then((_) => print('f8'));
//聲明了一個(gè)匿名Future
Future(() => print('f9'));
//往執(zhí)行體為null的fx注冊(cè)了了一個(gè)then
fx.then((_) => print('f10'));
//啟動(dòng)一個(gè)微任務(wù)
scheduleMicrotask(() => print('f11'));
print('f12');
先別急往下看結(jié)果,自己在小本本上寫寫空幻,或者自己敲代碼運(yùn)行一下
運(yùn)行一下郭膛,上述各個(gè)異步任務(wù)會(huì)依次打印其內(nèi)部執(zhí)行結(jié)果:
f12
f11
f1
f10
f2
f3
f5
f4
f6
f9
f7
f8
看到這兒,你可能已經(jīng)懵了氛悬。別急则剃,我們先來(lái)看一下這段代碼執(zhí)行過(guò)程中耘柱,Event Queue 與 Microtask Queue 中的變化情況,依次分析一下它們的執(zhí)行順序?yàn)槭裁磿?huì)是這樣的:
- 因?yàn)槠渌Z(yǔ)句都是異步任務(wù)棍现,所以先打印 f12调煎。
- 剩下的異步任務(wù)中,微任務(wù)隊(duì)列優(yōu)先級(jí)最高己肮,因此隨后打印 f11士袄;然后按照 Future 聲明的先后順序,打印 f1谎僻。
- 隨后到了 fx娄柳,由于 fx 的執(zhí)行體是 null,相當(dāng)于執(zhí)行完畢了艘绍,Dart 將 fx 的 then 放入微任務(wù)隊(duì)列赤拒,由于微任務(wù)隊(duì)列的優(yōu)先級(jí)最高,因此 fx 的 then 還是會(huì)最先執(zhí)行诱鞠,打印 f10挎挖。
- 然后到了 fx 下面的 f2,打印 f2航夺,然后執(zhí)行 then蕉朵,打印 f3。f4 是一個(gè)微任務(wù)阳掐,要到下一個(gè)事件循環(huán)才執(zhí)行始衅,因此后續(xù)的 then 繼續(xù)同步執(zhí)行,打印 f5缭保。本次事件循環(huán)結(jié)束汛闸,下一個(gè)事件循環(huán)取出 f4 這個(gè)微任務(wù),打印 f4涮俄。
- 然后到了 f2 下面的 f6蛉拙,打印 f6,然后執(zhí)行 then彻亲。這里需要注意的是孕锄,這個(gè) then 是一個(gè) Future 異步任務(wù),因此這個(gè) then苞尝,以及后續(xù)的 then 都被放入到事件隊(duì)列中了畸肆。后續(xù)的then 也被放到事件隊(duì)列 是因?yàn)?箭頭函數(shù) return了個(gè)future,所以后續(xù)的then是跟著他的宙址,如果沒(méi)有return future的話轴脐,then 會(huì)跟著前面的同步執(zhí)行
- f6 下面還有 f9,打印 f9。
- 最后一個(gè)事件循環(huán)大咱,打印 f7恬涧,以及后續(xù)的 f8。
你只需要記住一點(diǎn):then 會(huì)在 Future 函數(shù)體執(zhí)行完畢后立刻執(zhí)行碴巾,無(wú)論是共用同一個(gè)事件循環(huán)還是進(jìn)入下一個(gè)微任務(wù)溯捆。
在深入理解 Future 異步任務(wù)的執(zhí)行規(guī)則之后,我們?cè)賮?lái)看看怎么封裝一個(gè)異步函數(shù)厦瓢。
異步函數(shù)
對(duì)于一個(gè)異步函數(shù)來(lái)說(shuō)提揍,其返回時(shí)內(nèi)部執(zhí)行動(dòng)作并未結(jié)束,因此需要返回一個(gè) Future 對(duì)象煮仇,供調(diào)用者使用劳跃。調(diào)用者根據(jù) Future 對(duì)象,來(lái)決定:是在這個(gè) Future 對(duì)象上注冊(cè)一個(gè) then浙垫,等 Future 的執(zhí)行體結(jié)束了以后再進(jìn)行異步處理刨仑;還是一直同步等待 Future 執(zhí)行體結(jié)束。
對(duì)于異步函數(shù)返回的 Future 對(duì)象绞呈,如果調(diào)用者決定同步等待贸人,則需要在調(diào)用處使用 await 關(guān)鍵字间景,并且在調(diào)用處的函數(shù)體使用 async 關(guān)鍵字佃声。
在下面的例子中,異步方法延遲 3 秒返回了一個(gè) Hello 2019倘要,在調(diào)用處我們使用 await 進(jìn)行持續(xù)等待圾亏,等它返回了再打印:
//聲明了一個(gè)延遲3秒返回Hello的Future封拧,并注冊(cè)了一個(gè)then返回拼接后的Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:3), () => "Hello")
.then((x) => "$x 2019");
main() async{
String str = await fetchContent()
print(str);//等待Hello 2019的返回
}
也許你已經(jīng)注意到了志鹃,我們?cè)谑褂?await 進(jìn)行等待的時(shí)候,在等待語(yǔ)句的調(diào)用上下文函數(shù) main 加上了 async 關(guān)鍵字泽西。為什么要加這個(gè)關(guān)鍵字呢曹铃?
因?yàn)?Dart 中的 await 并不是阻塞等待,而是異步等待捧杉。Dart 會(huì)將調(diào)用體的函數(shù)也視作異步函數(shù)陕见,將等待語(yǔ)句的上下文放入 Event Queue 中,一旦有了結(jié)果味抖,Event Loop 就會(huì)把它從 Event Queue 中取出评甜,等待代碼繼續(xù)執(zhí)行。
我們先來(lái)看下這段代碼仔涩。第二行的 then 執(zhí)行體 f2 是一個(gè) Future忍坷,為了等它完成再進(jìn)行下一步操作,我們使用了 await,期望打印結(jié)果為 f1佩研、f2柑肴、f3、f4:
Future(() => print('f1'))
.then((_) async => await Future(() => print('f2')))
.then((_) => print('f3'));
Future(() => print('f4'));
實(shí)際上旬薯,當(dāng)你運(yùn)行這段代碼時(shí)就會(huì)發(fā)現(xiàn)嘉抒,打印出來(lái)的結(jié)果其實(shí)是 f1、f4袍暴、f2些侍、f3!
分析一下這段代碼的執(zhí)行順序:
- 按照任務(wù)的聲明順序政模,f1 和 f4 被先后加入事件隊(duì)列岗宣。
- f1 被取出并打印淋样;然后到了 then耗式。then 的執(zhí)行體是個(gè) future f2,于是放入 Event Queue趁猴。然后把 await 也放到 Event Queue 里刊咳。
- 這個(gè)時(shí)候要注意了,Event Queue 里面還有一個(gè) f4儡司,我們的 await 并不能阻塞 f4 的執(zhí)行娱挨。因此,Event Loop 先取出 f4捕犬,打印 f4跷坝;然后才能取出并打印 f2,最后把等待的 await 取出碉碉,開(kāi)始執(zhí)行后面的 f3柴钻。
由于 await 是采用事件隊(duì)列的機(jī)制實(shí)現(xiàn)等待行為的,所以比它先在事件隊(duì)列中的 f4 并不會(huì)被它阻塞垢粮。
接下來(lái)贴届,我們?cè)倏戳硪粋€(gè)例子:在主函數(shù)調(diào)用一個(gè)異步函數(shù)去打印一段話,而在這個(gè)異步函數(shù)中蜡吧,我們使用 await 與 async 同步等待了另一個(gè)異步函數(shù)返回字符串:
//聲明了一個(gè)延遲2秒返回Hello的Future毫蚓,并注冊(cè)了一個(gè)then返回拼接后的Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:2), () => "Hello")
.then((x) => "$x 2019");
//異步函數(shù)會(huì)同步等待Hello 2019的返回,并打印
func() async => print(await fetchContent());
main() {
print("func before");
func();
print("func after");
}
運(yùn)行這段代碼斩跌,我們發(fā)現(xiàn)最終輸出的順序其實(shí)是“func before”“func after”“Hello 2019”绍些。func 函數(shù)中的等待語(yǔ)句似乎沒(méi)起作用。這是為什么呢耀鸦?
我來(lái)給你分析一下這段代碼的執(zhí)行順序:
- 首先柬批,第一句代碼是同步的啸澡,因此先打印“func before”。
- 然后氮帐,進(jìn)入 func 函數(shù)嗅虏,func 函數(shù)調(diào)用了異步函數(shù) fetchContent,并使用 await 進(jìn)行等待上沐,因此我們把 fetchContent皮服、await 語(yǔ)句的上下文函數(shù) func 先后放入事件隊(duì)列。
- await 的上下文函數(shù)并不包含調(diào)用棧参咙,因此 func 后續(xù)代碼繼續(xù)執(zhí)行龄广,打印“func after”。
- 2 秒后蕴侧,fetchContent 異步任務(wù)返回“Hello 2019”择同,于是 func 的 await 也被取出,打印“Hello 2019”净宵。
通過(guò)上述分析敲才,你發(fā)現(xiàn)了什么現(xiàn)象?那就是 await 與 async 只對(duì)調(diào)用上下文的函數(shù)有效择葡,并不向上傳遞紧武。因此對(duì)于這個(gè)案例而言,func 是在異步等待敏储。如果我們想在 main 函數(shù)中也同步等待阻星,需要在調(diào)用異步函數(shù)時(shí)也加上 await,在 main 函數(shù)也加上 async虹曙。
各位大佬 應(yīng)該還是迷糊迫横,多看幾遍番舆,多敲幾遍 自然會(huì)懂得
總結(jié)
在 UI 編程過(guò)程中酝碳,異步和多線程是兩個(gè)相伴相生的名詞,也是很容易混淆的概念恨狈。對(duì)于異步方法調(diào)用而言疏哗,代碼不需要等待結(jié)果的返回,而是通過(guò)其他手段(比如通知禾怠、回調(diào)返奉、事件循環(huán)或多線程)在后續(xù)的某個(gè)時(shí)刻主動(dòng)(或被動(dòng))地接收?qǐng)?zhí)行結(jié)果。
因此吗氏,從辯證關(guān)系上來(lái)看芽偏,異步與多線程并不是一個(gè)同等關(guān)系:異步是目的,多線程只是我們實(shí)現(xiàn)異步的一個(gè)手段之一弦讽。而在 Flutter 中污尉,借助于 UI 框架提供的事件循環(huán)膀哲,我們可以不用阻塞的同時(shí)等待多個(gè)異步任務(wù),因此并不需要開(kāi)多線程被碗。我們一定要記住這一點(diǎn)某宪。
Q/A
- 假如有一個(gè)任務(wù)(讀寫文件或者網(wǎng)絡(luò))耗時(shí)10秒,并且加入到了事件任務(wù)隊(duì)列中锐朴,執(zhí)行單這個(gè)任務(wù)的時(shí)候不就把線程卡主嗎兴喂?
文件I/O和網(wǎng)絡(luò)調(diào)用并不是在Dart層做的,而是由操作系統(tǒng)提供的異步線程焚志,他倆把活兒干完之后把結(jié)果剛到隊(duì)列中衣迷,Dart代碼只是執(zhí)行一個(gè)簡(jiǎn)單的讀動(dòng)作。
單線程模型是指的事件隊(duì)列模型酱酬,和繪制界面的線程是一個(gè)嗎
我們所說(shuō)的單線程指的是主Isolate蘑险。而GPU繪制指令有單獨(dú)的線程執(zhí)行,跟主Isolate無(wú)關(guān)岳悟。事實(shí)上Flutter提供了4種task runner佃迄,有獨(dú)立的線程去運(yùn)行專屬的任務(wù):
1.Platform Task Runner:處理來(lái)自平臺(tái)(Android/iOS)的消息
2.UI Task Runner:執(zhí)行渲染邏輯、處理native plugin的消息贵少、timer呵俏、microtask、異步I/O操作處理等
3.GPU Task Runner:執(zhí)行GPU指令
4.IO Task Runner:執(zhí)行I/O任務(wù)
除此之外滔灶,操作系統(tǒng)本身也提供了大量異步并發(fā)機(jī)制普碎,可以利用多線程去執(zhí)行任務(wù)(比如socket),我們?cè)谥鱅solate中無(wú)需關(guān)心(如果真想主動(dòng)創(chuàng)建并發(fā)任務(wù)也可以)
// 第一段
Future(() => print('f6'))
.then((_) => Future(() => print('f7')))
.then((_) => print('f8'));
執(zhí)行結(jié)果為:f6 f7 f8
// 第二段
Future(() => print('f6'))
.then((_) {
Future(() => print('f7'));
})
.then((_) => print('f8'));
執(zhí)行結(jié)果為:f6 f8 f7
上面這兩段代碼為什么執(zhí)行結(jié)果不一樣呢录平?
單行箭頭函數(shù)是Future麻车,和函數(shù)體里有Future不是一回事
then函數(shù)會(huì)返回future,所以用=>的寫法時(shí)斗这, 新創(chuàng)建的future會(huì)被then返回动猬,第二個(gè)then的調(diào)用者就是新的future對(duì)象, 所以第二個(gè)then就不跟著原先的future 而是跟著它的新future了表箭。
如果把=>換成大括號(hào)赁咙,此時(shí)第二個(gè)then還是跟著原先的future,和新future無(wú)關(guān)
-
下一個(gè)奇異的是什么時(shí)候
2045年