本文首發(fā)于語(yǔ)雀https://www.yuque.com/hysteria/oemyze/cusxgq,轉(zhuǎn)載請(qǐng)注明出處鹦付。
在學(xué)習(xí)Java的時(shí)候,多線程是我們望而卻步的東西贺奠,但是接觸了Dart之后,發(fā)現(xiàn)它是單線程雕蔽。但其實(shí)這個(gè)單線程的運(yùn)行模型也包含非常多的內(nèi)容在里面粘咖,同樣讓人不想繼續(xù)看下去汁政。(回家種地警告)
但是作為Flutter中重要的一部分,我們必須要研究明白才能深入其整個(gè)宏觀世界玖雁,因此這個(gè)系列更扁,將從幾部分來(lái)展開(kāi)分析一下Flutter中的異步編程。
這里我分成兩大部分來(lái)分析這個(gè)事情赫冬。第一部分是Dart的異步模型浓镜,第二個(gè)是上層封裝。
異步模型分析
我們知道在一個(gè)稍微復(fù)雜點(diǎn)的程序運(yùn)行時(shí)面殖,總是會(huì)伴有一些網(wǎng)絡(luò)竖哩,IO的操作。而在GUI系統(tǒng)中脊僚,如果這類(lèi)耗時(shí)操作被放到主線程來(lái)執(zhí)行相叁,那么用戶的操作就會(huì)無(wú)法及時(shí)響應(yīng),這個(gè)肯定是不能夠接受的辽幌。而就算在Server工程中增淹,如果用單線程的處理請(qǐng)求也是該被勸退了。而多線程帶來(lái)的問(wèn)題也是非常多乌企,這就涉及到更多的虑润,如同步鎖問(wèn)題,線程池等等……所以多線程在各自的技術(shù)棧中都是非常復(fù)雜的一部分加酵。而今天的主角Dart拳喻,偏偏就選擇了單線程。
這個(gè)對(duì)Dart開(kāi)發(fā)者實(shí)在是太友好了猪腕,不用考慮太多關(guān)于多線程的問(wèn)題就可以完成復(fù)雜的異步操作冗澈。但是話又說(shuō)回來(lái),如果是單線程陋葡,上面說(shuō)的GUI問(wèn)題豈不是就出現(xiàn)了么亚亲?其實(shí)不然,我們可以繼續(xù)往下看。
首先我們來(lái)考慮一個(gè)問(wèn)題捌归。多線程模型里肛响,實(shí)現(xiàn)GUI交互是怎么樣的。這里以點(diǎn)擊按鈕請(qǐng)求數(shù)據(jù)更新界面舉例惜索。
主線程點(diǎn)擊按鈕 -> 創(chuàng)建子線程進(jìn)行網(wǎng)絡(luò)請(qǐng)求 -> 線程通信發(fā)送數(shù)據(jù) -> 更新GUI
如上所示特笋,我們說(shuō)常見(jiàn)的Hander其實(shí)就是線程通信方式的一種。那么在單線程模型中门扇,Dart雹有,或者JavaScript則是借助于單線程循環(huán)模型來(lái)實(shí)現(xiàn)這個(gè)操作的偿渡。
先不說(shuō)這個(gè)模型是啥樣子臼寄,到這里很多同學(xué)就開(kāi)始有疑問(wèn)了。這不卡主線程溜宽?耗時(shí)操作怎么辦的啊喂吉拳。但其實(shí)仔細(xì)想想,我們并不是每時(shí)每刻都在與界面進(jìn)行交互适揉,也并不是無(wú)時(shí)無(wú)刻在進(jìn)行網(wǎng)絡(luò)與IO操作留攒,這就決定了程序在大部分時(shí)間都是空閑的。既然如此嫉嘀,那么用戶交互炼邀,界面繪制與網(wǎng)絡(luò)請(qǐng)求就能夠被安排在一個(gè)進(jìn)程中。這時(shí)候你可能又會(huì)說(shuō)了剪侮,就算可以拭宁,那他在網(wǎng)絡(luò)請(qǐng)求的時(shí)候遇到用戶交互事件怎么辦?那豈不是還是不能響應(yīng)瓣俯。這就不得不提到操作系統(tǒng)中一個(gè)非常重要的概念了 -- "時(shí)間片"杰标。也就是說(shuō),操作系統(tǒng)不會(huì)讓某個(gè)線程無(wú)休止的運(yùn)行直到結(jié)束彩匕,而是將任務(wù)切成不同的時(shí)間片腔剂,某一時(shí)刻運(yùn)行一個(gè)線程的其中一片。給人造成多線程并行執(zhí)行的假象驼仪,這其實(shí)也就是“并發(fā)”的概念掸犬。這里說(shuō)的是多線程,那么我們繼續(xù)往微觀角度想绪爸,如果一個(gè)線程里的n個(gè)任務(wù)單元可以如此湾碎,那豈不是就可以給人一種多個(gè)任務(wù)在一起運(yùn)行的假象呢?嗯毡泻,有人比你先想到了胜茧,于是就有了協(xié)程。那么先不說(shuō)Dart里面這個(gè)叫不叫做協(xié)程,但是結(jié)論就是這樣了呻顽。
運(yùn)行為什么不卡頓的問(wèn)題解決了雹顺,那么還有一個(gè)問(wèn)題。單線程模型能夠利用好多核的能力么廊遍?這個(gè)后面會(huì)做解答嬉愧。
Dart異步模型
接下來(lái)就是大家看過(guò)很多次的異步模型了,這里我從別的文章上“借鑒”了一張圖喉前。
從圖中可以看出模型中有兩個(gè)隊(duì)列没酣,事件隊(duì)列(Event Queue)與微任務(wù)隊(duì)列(Microtask Queue)。而這個(gè)模型的運(yùn)行規(guī)則是卵迂。
- 啟動(dòng)App
- 首先執(zhí)行main方法里的代碼裕便。
- main方法執(zhí)行完成之后,開(kāi)始遍歷執(zhí)行微任務(wù)隊(duì)列见咒,直到微任務(wù)為空偿衰。
- 繼續(xù)向下執(zhí)行事件隊(duì)列,執(zhí)行完一個(gè)就查詢(xún)微任務(wù)隊(duì)列是否有可以執(zhí)行的微任務(wù)
- 然后兩個(gè)隊(duì)列的執(zhí)行就一直按照這樣的循環(huán)方式執(zhí)行下去改览,直到App退出
那么相比到這里大家開(kāi)始疑問(wèn)什么樣的叫做微任務(wù)下翎,什么樣的又可以稱(chēng)為事件?下面就解釋一下這兩種的區(qū)別宝当,以及為什么要設(shè)計(jì)兩個(gè)隊(duì)列视事。
微任務(wù)
微任務(wù)在圖中是一個(gè)優(yōu)先級(jí)非常高的角色,可以看到庆揩。每次都是微任務(wù)優(yōu)先執(zhí)行俐东,一有微任務(wù),不過(guò)是先來(lái)的后來(lái)的都需要無(wú)條件執(zhí)行盾鳞。微任務(wù)可以通過(guò)下面的方式加入犬性。
scheduleMicrotask(() => print('This is a microtask'));
考慮到這個(gè)任務(wù)的優(yōu)先級(jí)比較高,我們平時(shí)也不會(huì)用這種方法來(lái)執(zhí)行異步任務(wù)腾仅。Flutter內(nèi)部也只有幾處用到了這個(gè)方法(比如乒裆,手勢(shì)識(shí)別、文本輸入推励、滾動(dòng)視圖鹤耍、保存頁(yè)面效果等需要高優(yōu)執(zhí)行任務(wù)的場(chǎng)景)。
事件
事件的范圍就廣了一點(diǎn)验辞,比如網(wǎng)絡(luò)稿黄,IO,用戶操作跌造,繪圖杆怕,計(jì)時(shí)器等族购。而這個(gè)事件還有一個(gè)重要封裝,就是Future陵珍,從名字可以看出含義就是未來(lái)執(zhí)行的一段代碼寝杖。
為什么單線程?
結(jié)合單線程模型和之前說(shuō)的協(xié)程部分我們可以大概知道了Dart的運(yùn)行規(guī)則互纯。這個(gè)時(shí)候我們大概可以解答之前留下的疑問(wèn)了瑟幕,Dart的單線程模型怎么發(fā)揮CPU多核優(yōu)勢(shì)呢?
下面是我個(gè)人的一點(diǎn)見(jiàn)解留潦,如果有不同的觀點(diǎn)可以指出只盹。
其實(shí)我們看JavaScript為什么用單線程模型,也就知道Dart為什么也要用了兔院。Dart誕生是為了“Battle”JS的殖卑,但目前看來(lái)應(yīng)該是失敗了。我們從網(wǎng)上查閱資料秆乳,就會(huì)發(fā)現(xiàn)這樣的段落懦鼠。
JavaScript的單線程,與它的用途有關(guān)屹堰。作為瀏覽器腳本語(yǔ)言,JavaScript的主要用途是與用戶互動(dòng)街氢,以及操作DOM扯键。這決定了它只能是單線程,否則會(huì)帶來(lái)很復(fù)雜的同步問(wèn)題珊肃。比如荣刑,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容伦乔,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn)厉亏,這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
為了利用多核CPU的計(jì)算能力烈和,HTML5提出Web Worker標(biāo)準(zhǔn)爱只,允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制招刹,且不得操作DOM恬试。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變JavaScript單線程的本質(zhì)疯暑。
這兩段話說(shuō)到了兩個(gè)事训柴。一個(gè)是多線程容易發(fā)生并發(fā)問(wèn)題,第二個(gè)是JS也嘗試?yán)枚嗪薈PU的能力妇拯,但是也只是閹割版的多線程幻馁。因此我們也就可以大致類(lèi)比到,Dart其實(shí)也是基于此考慮的,畢竟Dart生來(lái)是想用在Web的仗嗦,但是最后用在了移動(dòng)端预麸。但其實(shí)恰好所有的GUI系統(tǒng)都不是特別需要非常多的線程的,(相信許多Android開(kāi)發(fā)者都沒(méi)怎么用過(guò)多線程的鎖之類(lèi)的吧)儒将,最常見(jiàn)的也就是2吏祸,3個(gè)線程在做事情。但是退一萬(wàn)步講钩蚊,就真的是非常復(fù)雜的贡翘,要開(kāi)許多個(gè)線程怎么辦?也就是說(shuō)情況越極端砰逻,對(duì)CPU的利用能力與原生差異就越明顯鸣驱。這個(gè)時(shí)候Dart其實(shí)也考慮過(guò)了,就是它還是運(yùn)行你創(chuàng)建線程的蝠咆。
這個(gè)“線程”叫做isolate踊东。
isolate
isolate在這里翻譯成“隔離”,從名字就可以看出來(lái)刚操,不同的isolate都是獨(dú)立的闸翅。這個(gè)與你說(shuō)認(rèn)知的Thread是有差異的。所以Dart還是保守了菊霜。難道是怕寫(xiě)不出DougLea老爺子那么優(yōu)雅的代碼坚冀?沒(méi)有了多線程的共享問(wèn)題,也就不用寫(xiě)各種同步鎖鉴逞,CAS原子等機(jī)制记某,但隨之帶來(lái)的問(wèn)題就是通信了。isolate的通信是靠著port的构捡,這里不展開(kāi)說(shuō)液南。所以更像是 Future是線程,isolate是進(jìn)程勾徽。
到這里我們可以拋下的疑問(wèn)都解決了滑凉。
可以看到,單線程模型與多線程模型沒(méi)有孰好孰壞捂蕴,只有在他們各自擅長(zhǎng)的場(chǎng)景才能展示出自己最大的的性?xún)r(jià)比譬涡。也正是如此,我們?cè)贏ndroid開(kāi)發(fā)中用多線程的時(shí)候啥辨,也不是盲目的去new Thread涡匀,而是優(yōu)先會(huì)考慮線程池。大部分情況下溉知,適當(dāng)?shù)木€程可以更好的利用CPU不會(huì)消耗很大的資源陨瘩,而且也能夠得心應(yīng)手的處理完所有任務(wù)腕够。不會(huì)造成資源的浪費(fèi)。
平時(shí)開(kāi)發(fā)業(yè)務(wù)很少用到isolate舌劳,一方面是它通信很麻煩帚湘,另一方面我們并沒(méi)有太大的需求要用這個(gè),但是如果真的有需要的場(chǎng)景甚淡,其實(shí)是不建議盲目用一堆Future的大诸,這樣除了代碼簡(jiǎn)單之外,沒(méi)有什么好處贯卦。
Future的分析(1)
前面說(shuō)到Future其實(shí)是對(duì)事件的上層封裝资柔,但是實(shí)際的運(yùn)行過(guò)程也有不一樣的表現(xiàn)。為什么這么說(shuō)撵割,可以看到下面的分析贿堰。首先我們從Future這個(gè)類(lèi)說(shuō)起。
首先我們看到啡彬,F(xiàn)uture是有幾個(gè)構(gòu)造方法的羹与,此外沒(méi)有在這個(gè)圖片上表現(xiàn)出來(lái)的是他的默認(rèn)構(gòu)造,下面分別來(lái)說(shuō)一下這幾個(gè)構(gòu)造方法庶灿。
Future(FutureOr<T> computation())
默認(rèn)構(gòu)造函數(shù)纵搁,此函數(shù)接受一個(gè)返回FutureOr類(lèi)型的函數(shù)類(lèi)型
factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
通過(guò)這個(gè)方法創(chuàng)建的Future,最后會(huì)被添加到EventQueue中跳仿。而這里FutureOr這個(gè)類(lèi)其實(shí)就是以這個(gè)名字來(lái)告訴你诡渴,可以返回包括Future在內(nèi)的所有類(lèi)型,其實(shí)并沒(méi)有相應(yīng)的實(shí)現(xiàn)菲语。這里由Timer.run來(lái)調(diào)度了computation參數(shù)。
Future.microtask(FutureOr<T> computation())
微任務(wù)構(gòu)造惑灵。
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
從代碼看山上,不一樣的地方在于這次是使用scheduleMicrotask來(lái)調(diào)度的,我們前面說(shuō)過(guò)英支,通過(guò)這個(gè)方法就可以創(chuàng)建一個(gè)微任務(wù)佩憾,因此這個(gè)computation方法將會(huì)被添加到微任務(wù)隊(duì)列中執(zhí)行。
這里來(lái)一個(gè)小例子看一下誰(shuí)更快被執(zhí)行干花。
void main(){
Future(() => print("event task!"));
Future.microtask(() => print("micro task!"));
}
事實(shí)證明妄帘,micro task確實(shí)會(huì)優(yōu)先被調(diào)度。
Future.sync(FutureOr<T> computation()) {}
看名字是一個(gè)同步的方法池凄。
factory Future.sync(FutureOr<T> computation()) {
try {
var result = computation();
if (result is Future<T>) {
return result;
} else {
return new _Future<T>.value(result as dynamic);
}
} catch (error, stackTrace) {
……
}
}
這個(gè)方法是直接取了computation結(jié)果抡驼,如果結(jié)果是Future,就直接返回肿仑,否則使用value方法調(diào)度致盟。
這里可以再做個(gè)對(duì)比碎税。
void main(){
Future(() => print("event task!"));
Future.microtask(() => print("micro task!"));
Future.sync(() {
print("sync task");
});
}
由于sync方法的參數(shù)提前被執(zhí)行,就相當(dāng)于在main方法層面執(zhí)行的馏锡,這個(gè)順序也與我們上面提到的線程模型完全相符雷蹂。
Future.value([FutureOr<T>? value])
這個(gè)方法上面提到過(guò)了,而他的實(shí)現(xiàn)也比較特殊杯道。
factory Future.value([FutureOr<T>? value]) {
return new _Future<T>.immediate(value == null ? value as T : value);
}
_Future.immediate(FutureOr<T> result) : _zone = Zone._current {
_asyncComplete(result);
}
void _asyncComplete(FutureOr<T> value) {
if (value is Future<T>) {
_chainFuture(value);
return;
}
_asyncCompleteWithValue(value as dynamic);
}
void _asyncCompleteWithValue(T value) {
_setPendingComplete();
//這里~
_zone.scheduleMicrotask(() {
_completeWithValue(value);
});
}
可以看到這個(gè)方法匪煌,如果是返回非Future類(lèi)型,則最終調(diào)用了scheduleMicrotask將任務(wù)調(diào)度党巾。這樣做也是有其中的原因的萎庭,因?yàn)榉荈uture的value不需要執(zhí)行,也就認(rèn)為傳入即完成昧港,則需要迅速執(zhí)行其后的鏈?zhǔn)椒椒ㄇ嬉枰玫轿⑷蝿?wù)隊(duì)列。
與此情況類(lèi)似的一種是這樣的创肥。如果我們提前建立了一個(gè)Future达舒,并且這個(gè)Future已經(jīng)執(zhí)行完成的時(shí)候,其后的then的調(diào)用則會(huì)被微任務(wù)隊(duì)列調(diào)度叹侄。
var future = Future(() => print("future"));
future.then((value) => print("future then"));
Future.delayed(Duration duration, [FutureOr<T> computation()?])
從函數(shù)名字中就可以看出來(lái)巩搏,這是一個(gè)延時(shí)執(zhí)行的Future。
factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
……
_Future<T> result = new _Future<T>();
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
相信大家已經(jīng)可以猜到趾代,內(nèi)部與默認(rèn)構(gòu)造函數(shù)幾乎一樣贯底,只是利用了Timer的計(jì)時(shí)功能,時(shí)間到了之后開(kāi)始調(diào)度撒强。
Future.error(Object error, [StackTrace? stackTrace])
這個(gè)方法不太常用禽捆,是創(chuàng)建一個(gè)錯(cuò)誤的Future,內(nèi)部同value方法飘哨,也是由scheduleMicrotask進(jìn)行調(diào)度的胚想,至于這個(gè)方法存在的意思是什么,我也不太清楚了芽隆。
上面是一些基礎(chǔ)的構(gòu)造/工廠函數(shù)浊服,用來(lái)創(chuàng)建Future,但是Future也提供了一些靜態(tài)的方法胚吁,用于創(chuàng)建更高級(jí)的表現(xiàn)形式牙躺。
Future.wait
這個(gè)方法用來(lái)等待多個(gè)Future的結(jié)果,如果其中一個(gè)發(fā)生了問(wèn)題腕扶,那么就直接失敗孽拷。但是這個(gè)表現(xiàn)由eagerError參數(shù)來(lái)控制
Future.foreach
這個(gè)方法其實(shí)就算是工具了,類(lèi)似于RX里的一些工具方法蕉毯,循環(huán)遍歷列表乓搬,然后每次讀取到一個(gè)數(shù)據(jù)思犁,就調(diào)用一下回調(diào)。
Future.forEach({1,2,3}, (num){
return Future.delayed(Duration(seconds: num),(){print(num);});
});
Future.any
返回第一個(gè)Future執(zhí)行完的結(jié)果进肯,不管這個(gè)結(jié)果是正確與否激蹲。
static Future<T> any<T>(Iterable<Future<T>> futures) {}
Future.doWhile
循環(huán)執(zhí)行回調(diào)操作,直到它返回false
static Future doWhile(FutureOr<bool> action())
Future的分析(2)
上面的非常大的篇幅來(lái)分析了幾個(gè)Future類(lèi)里的API江掩,我們?cè)谄綍r(shí)的開(kāi)發(fā)中也就是利用這些API來(lái)完成的学辱。但這些API只是用來(lái)創(chuàng)建Future,如果我們使用Future發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求环形,怎么能拿到請(qǐng)求返回的結(jié)果呢策泣?這里就要用到我們的處理結(jié)果相關(guān)方法。而且這些方法的也會(huì)有一些配合的規(guī)律抬吟,一起來(lái)看下萨咕。
then
前面提到過(guò)then這個(gè)方法。
他就是用來(lái)處理結(jié)果的火本,當(dāng)我們的耗時(shí)任務(wù)執(zhí)行完成的時(shí)候危队,then就會(huì)被調(diào)用,而且多個(gè)then鏈在一起的話钙畔,還會(huì)一起調(diào)用茫陆。
//簽名
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
//使用
Future(() => print("task1"))
.then((value) {
print("task2");
Future.microtask(() => print("micro task"));
})
.then((value) => print("task4"))
.then((value) => print("task5"));
這里Dart Pad這個(gè)工具沒(méi)法使用scheduleMicrotask這個(gè)方法,所有用Future.microtask代替擎析,結(jié)果也是符合預(yù)期的簿盅。可以看到第一個(gè)then里面就調(diào)度了微任務(wù)揍魂,為什么沒(méi)有立馬執(zhí)行而是執(zhí)行了后續(xù)的then呢桨醋?這里then的內(nèi)容是會(huì)被優(yōu)先執(zhí)行完的,因?yàn)榇藭r(shí)Future已經(jīng)執(zhí)行完成了现斋,需要立馬進(jìn)行回調(diào)讨盒,不能進(jìn)行額外的等待,所以看起來(lái)幾個(gè)then是在一起執(zhí)行的步责。但是這種情況下,還和Future.value和 future.then這兩種情況不一樣禀苦,這個(gè)例子所有的內(nèi)容都是會(huì)在Event Queue中執(zhí)行的蔓肯。
你可以把他們想象成,所有的then的代碼內(nèi)容都在Future的耗時(shí)任務(wù)回調(diào)中振乏,會(huì)在調(diào)度的時(shí)候一起被放到事件隊(duì)列蔗包。
但是……又有特殊情況了,如果then返回了一個(gè)Future,那么后續(xù)的then是不會(huì)被立馬執(zhí)行的慧邮,而是排在這個(gè)Future之后的调限。
catchError
如果Future發(fā)生了異常舟陆,則需要使用catchError來(lái)捕獲。
whenComplete
類(lèi)似于Java的finally耻矮,無(wú)論成功和失敗總會(huì)調(diào)用到的一個(gè)方法秦躯。
timeout
Timeout接受一個(gè)Duration類(lèi)型的值,用來(lái)設(shè)置超時(shí)時(shí)間裆装。如果Future在超時(shí)時(shí)間內(nèi)完成踱承,則就返回原Future的值,如果到達(dá)超時(shí)時(shí)間還沒(méi)有完成哨免,就是拋出TimeoutException異常茎活,當(dāng)然,如果設(shè)置了onTimeout參數(shù)琢唾,就會(huì)以設(shè)置的返回值返回载荔,不會(huì)產(chǎn)生異常。
Future<T> timeout(Duration timeLimit, {FutureOr<T> onTimeout()?});
總結(jié)
未完待續(xù)采桃。
干就完了懒熙。