Flutter之Future 異步await sync 終結(jié)者

20年圣誕

2020 神奇的一年,載入史冊的一年,改變了很多人的命運,曼谷第一開膛手也轉(zhuǎn)行入坑Flutter,廢話不多說,干就完了

Event Loop 機(jī)制

像iOS runloop一樣,flutter也有事件循環(huán),不同的是,那就是 Dart 是單線程的。那單線程意味著什么呢?這意味著 Dart 代碼是有序的,按照在 main 函數(shù)出現(xiàn)的次序一個接一個地執(zhí)行,不會被其他代碼中斷。另外,作為支持 Flutter 這個 UI 框架的關(guān)鍵技術(shù),Dart 當(dāng)然也支持異步。需要注意的是,單線程和異步并不沖突。

為什么單線程也可以異步?

比如說,網(wǎng)絡(luò)請求,Socket 本身提供了 select 模型可以異步查詢;而文件 IO,操作系統(tǒng)也提供了基于事件的回調(diào)機(jī)制。

基于這些特點,單線程模型可以在等待的過程中做別的事情,等真正需要響應(yīng)結(jié)果了,再去做對應(yīng)的處理。因為等待過程并不是阻塞的,所以給我們的感覺就像是同時在做多件事情一樣。但其實始終只有一個線程在處理你的事情。

等待這個行為是通過 Event Loop 驅(qū)動的。事件隊列 Event Queue 會把其他平行世界(比如 Socket)完成的,需要主線程響應(yīng)的事件放入其中。像其他語言一樣,Dart 也有一個巨大的事件循環(huán),在不斷的輪詢事件隊列,取出事件(比如,鍵盤事件、I\O 事件、網(wǎng)絡(luò)事件等),在主線程同步執(zhí)行其回調(diào)函數(shù),如下圖所示:

Event Loop
異步任務(wù)

圖 1 的 Event Loop 示意圖只是一個簡化版。在 Dart 中,實際上有兩個隊列,一個事件隊列(Event Queue),另一個則是微任務(wù)隊列(Microtask Queue)。在每一次事件循環(huán)中,Dart 總是先去第一個微任務(wù)隊列中查詢是否有可執(zhí)行的任務(wù),有的話會處理完所有的任務(wù)。如果沒有,才會處理后續(xù)的事件隊列的流程。

Event Loop 完整版的流程圖,應(yīng)該如下所示:


Microtask Queue 與 Event Queue

首先,我們看看微任務(wù)隊列。微任務(wù)顧名思義,表示一個短時間內(nèi)就會完成的異步任務(wù)。從上面的流程圖可以看到,微任務(wù)隊列在事件循環(huán)中的優(yōu)先級是最高的,只要隊列中還有任務(wù),就可以一直霸占著事件循環(huán)。

微任務(wù)是由 scheduleMicroTask 建立的。如下所示,這段代碼會在下一個事件循環(huán)中輸出一段字符串:


scheduleMicrotask(() => print('This is a microtask'));

一般的異步任務(wù)通常也很少必須要在事件隊列前完成,所以也不需要太高的優(yōu)先級,因此我們通常很少會直接用到微任務(wù)隊列,就連 Flutter 內(nèi)部,也只有 7 處用到了而已(比如,手勢識別、文本輸入、滾動視圖、保存頁面效果等需要高優(yōu)執(zhí)行任務(wù)的場景)。

異步任務(wù)我們用的最多的還是優(yōu)先級更低的 Event Queue。比如,I/O、繪制、定時器這些異步事件,都是通過事件隊列驅(qū)動主線程執(zhí)行的。

Dart 為 Event Queue 的任務(wù)建立提供了一層封裝,叫作 Future。從名字上也很容易理解,它表示一個在未來時間才會完成的任務(wù)。

把一個函數(shù)體放入 Future,就完成了從同步任務(wù)到異步任務(wù)的包裝。Future 還提供了鏈?zhǔn)秸{(diào)用的能力,可以在異步任務(wù)執(zhí)行完畢后依次執(zhí)行鏈路上的其他函數(shù)體。

接下來,我們看一個具體的代碼示例:分別聲明兩個異步任務(wù),在下一個事件循環(huán)中輸出一段字符串。其中第二個任務(wù)執(zhí)行完畢之后,還會繼續(xù)輸出另外兩段字符串:


Future(() => print('Running in Future 1'));//下一個事件循環(huán)輸出字符串

Future(() => print(‘Running in Future 2'))
  .then((_) => print('and then 1'))
  .then((_) => print('and then 2’));//上一個事件循環(huán)結(jié)束后,連續(xù)輸出三段字符串

當(dāng)然,這兩個 Future 異步任務(wù)的執(zhí)行優(yōu)先級比微任務(wù)的優(yōu)先級要低。

正常情況下,一個 Future 異步任務(wù)的執(zhí)行是相對簡單的:在我們聲明一個 Future 時,Dart 會將異步任務(wù)的函數(shù)執(zhí)行體放入事件隊列,然后立即返回,后續(xù)的代碼繼續(xù)同步執(zhí)行。而當(dāng)同步執(zhí)行的代碼執(zhí)行完畢后,事件隊列會按照加入事件隊列的順序(即聲明順序),依次取出事件,最后同步執(zhí)行 Future 的函數(shù)體及后續(xù)的 then。

這意味著,then 與 Future 函數(shù)體共用一個事件循環(huán)。而如果 Future 有多個 then,它們也會按照鏈?zhǔn)秸{(diào)用的先后順序同步執(zhí)行,同樣也會共用一個事件循環(huán)。

如果 Future 執(zhí)行體已經(jīng)執(zhí)行完畢了,但你又拿著這個 Future 的引用,往里面加了一個 then 方法體,這時 Dart 會如何處理呢?面對這種情況,Dart 會將后續(xù)加入的 then 方法體放入微任務(wù)隊列,盡快執(zhí)行。

下面的代碼演示了 Future 的執(zhí)行規(guī)則,即,先加入事件隊列,或者先聲明的任務(wù)先執(zhí)行;then 在 Future 結(jié)束后立即執(zhí)行。


//f1比f2先執(zhí)行
Future(() => print('f1'));
Future(() => print('f2'));

//f3執(zhí)行后會立刻同步執(zhí)行then 3
Future(() => print('f3')).then((_) => print('then 3'));

//then 4會加入微任務(wù)隊列,盡快執(zhí)行
Future(() => null).then((_) => print('then 4'));
  • 在第一個例子中,由于 f1 比 f2 先聲明,因此會被先加入事件隊列,所以 f1 比 f2 先執(zhí)行;
  • 在第二個例子中,由于 Future 函數(shù)體與 then 共用一個事件循環(huán),因此 f3 執(zhí)行后會立刻同步執(zhí)行 then 3;
  • 最后一個例子中,F(xiàn)uture 函數(shù)體是 null,這意味著它不需要也沒有事件循環(huán),因此后續(xù)的 then 也無法與它共享。在這種場景下,Dart 會把后續(xù)的 then 放入微任務(wù)隊列,在下一次事件循環(huán)中執(zhí)行。

通過一個綜合案例,來把之前介紹的各個執(zhí)行規(guī)則都串起來,再集中學(xué)習(xí)一下。
在下面的例子中,我們依次聲明了若干個異步任務(wù) Future,以及微任務(wù)。在其中的一些 Future 內(nèi)部,我們又內(nèi)嵌了 Future 與 microtask 的聲明:


Future(() => print('f1'));//聲明一個匿名Future
Future fx = Future(() =>  null);//聲明Future fx,其執(zhí)行體為null

//聲明一個匿名Future,并注冊了兩個then。在第一個then回調(diào)里啟動了一個微任務(wù)
Future(() => print('f2')).then((_) {
  print('f3');
  scheduleMicrotask(() => print('f4'));
}).then((_) => print('f5'));

//聲明了一個匿名Future,并注冊了兩個then。第一個then是一個Future
Future(() => print('f6'))
  .then((_) => Future(() => print('f7')))
  .then((_) => print('f8'));

//聲明了一個匿名Future
Future(() => print('f9'));

//往執(zhí)行體為null的fx注冊了了一個then
fx.then((_) => print('f10'));

//啟動一個微任務(wù)
scheduleMicrotask(() => print('f11'));
print('f12');

先別急往下看結(jié)果,自己在小本本上寫寫,或者自己敲代碼運行一下

運行一下,上述各個異步任務(wù)會依次打印其內(nèi)部執(zhí)行結(jié)果:


f12
f11
f1
f10
f2
f3
f5
f4
f6
f9
f7
f8

看到這兒,你可能已經(jīng)懵了。別急,我們先來看一下這段代碼執(zhí)行過程中,Event Queue 與 Microtask Queue 中的變化情況,依次分析一下它們的執(zhí)行順序為什么會是這樣的:

Event Queue 與 Microtask Queue 變化示例
  • 因為其他語句都是異步任務(wù),所以先打印 f12。
  • 剩下的異步任務(wù)中,微任務(wù)隊列優(yōu)先級最高,因此隨后打印 f11;然后按照 Future 聲明的先后順序,打印 f1。
  • 隨后到了 fx,由于 fx 的執(zhí)行體是 null,相當(dāng)于執(zhí)行完畢了,Dart 將 fx 的 then 放入微任務(wù)隊列,由于微任務(wù)隊列的優(yōu)先級最高,因此 fx 的 then 還是會最先執(zhí)行,打印 f10。
  • 然后到了 fx 下面的 f2,打印 f2,然后執(zhí)行 then,打印 f3。f4 是一個微任務(wù),要到下一個事件循環(huán)才執(zhí)行,因此后續(xù)的 then 繼續(xù)同步執(zhí)行,打印 f5。本次事件循環(huán)結(jié)束,下一個事件循環(huán)取出 f4 這個微任務(wù),打印 f4。
  • 然后到了 f2 下面的 f6,打印 f6,然后執(zhí)行 then。這里需要注意的是,這個 then 是一個 Future 異步任務(wù),因此這個 then,以及后續(xù)的 then 都被放入到事件隊列中了。后續(xù)的then 也被放到事件隊列 是因為 箭頭函數(shù) return了個future,所以后續(xù)的then是跟著他的,如果沒有return future的話,then 會跟著前面的同步執(zhí)行
  • f6 下面還有 f9,打印 f9。
  • 最后一個事件循環(huán),打印 f7,以及后續(xù)的 f8。

你只需要記住一點:then 會在 Future 函數(shù)體執(zhí)行完畢后立刻執(zhí)行,無論是共用同一個事件循環(huán)還是進(jìn)入下一個微任務(wù)。

在深入理解 Future 異步任務(wù)的執(zhí)行規(guī)則之后,我們再來看看怎么封裝一個異步函數(shù)。

異步函數(shù)

對于一個異步函數(shù)來說,其返回時內(nèi)部執(zhí)行動作并未結(jié)束,因此需要返回一個 Future 對象,供調(diào)用者使用。調(diào)用者根據(jù) Future 對象,來決定:是在這個 Future 對象上注冊一個 then,等 Future 的執(zhí)行體結(jié)束了以后再進(jìn)行異步處理;還是一直同步等待 Future 執(zhí)行體結(jié)束。

對于異步函數(shù)返回的 Future 對象,如果調(diào)用者決定同步等待,則需要在調(diào)用處使用 await 關(guān)鍵字,并且在調(diào)用處的函數(shù)體使用 async 關(guān)鍵字。

在下面的例子中,異步方法延遲 3 秒返回了一個 Hello 2019,在調(diào)用處我們使用 await 進(jìn)行持續(xù)等待,等它返回了再打?。?/p>


//聲明了一個延遲3秒返回Hello的Future,并注冊了一個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)注意到了,我們在使用 await 進(jìn)行等待的時候,在等待語句的調(diào)用上下文函數(shù) main 加上了 async 關(guān)鍵字。為什么要加這個關(guān)鍵字呢?

因為 Dart 中的 await 并不是阻塞等待,而是異步等待。Dart 會將調(diào)用體的函數(shù)也視作異步函數(shù),將等待語句的上下文放入 Event Queue 中,一旦有了結(jié)果,Event Loop 就會把它從 Event Queue 中取出,等待代碼繼續(xù)執(zhí)行。

我們先來看下這段代碼。第二行的 then 執(zhí)行體 f2 是一個 Future,為了等它完成再進(jìn)行下一步操作,我們使用了 await,期望打印結(jié)果為 f1、f2、f3、f4:


Future(() => print('f1'))
  .then((_) async => await Future(() => print('f2')))
  .then((_) => print('f3'));
Future(() => print('f4'));

實際上,當(dāng)你運行這段代碼時就會發(fā)現(xiàn),打印出來的結(jié)果其實是 f1、f4、f2、f3!

分析一下這段代碼的執(zhí)行順序:

  • 按照任務(wù)的聲明順序,f1 和 f4 被先后加入事件隊列。
  • f1 被取出并打??;然后到了 then。then 的執(zhí)行體是個 future f2,于是放入 Event Queue。然后把 await 也放到 Event Queue 里。
  • 這個時候要注意了,Event Queue 里面還有一個 f4,我們的 await 并不能阻塞 f4 的執(zhí)行。因此,Event Loop 先取出 f4,打印 f4;然后才能取出并打印 f2,最后把等待的 await 取出,開始執(zhí)行后面的 f3。

由于 await 是采用事件隊列的機(jī)制實現(xiàn)等待行為的,所以比它先在事件隊列中的 f4 并不會被它阻塞。

接下來,我們再看另一個例子:在主函數(shù)調(diào)用一個異步函數(shù)去打印一段話,而在這個異步函數(shù)中,我們使用 await 與 async 同步等待了另一個異步函數(shù)返回字符串:


//聲明了一個延遲2秒返回Hello的Future,并注冊了一個then返回拼接后的Hello 2019
Future<String> fetchContent() => 
  Future<String>.delayed(Duration(seconds:2), () => "Hello")
    .then((x) => "$x 2019");
//異步函數(shù)會同步等待Hello 2019的返回,并打印
func() async => print(await fetchContent());

main() {
  print("func before");
  func();
  print("func after");
}

運行這段代碼,我們發(fā)現(xiàn)最終輸出的順序其實是“func before”“func after”“Hello 2019”。func 函數(shù)中的等待語句似乎沒起作用。這是為什么呢?

我來給你分析一下這段代碼的執(zhí)行順序:

  • 首先,第一句代碼是同步的,因此先打印“func before”。
  • 然后,進(jìn)入 func 函數(shù),func 函數(shù)調(diào)用了異步函數(shù) fetchContent,并使用 await 進(jìn)行等待,因此我們把 fetchContent、await 語句的上下文函數(shù) func 先后放入事件隊列。
  • await 的上下文函數(shù)并不包含調(diào)用棧,因此 func 后續(xù)代碼繼續(xù)執(zhí)行,打印“func after”。
  • 2 秒后,fetchContent 異步任務(wù)返回“Hello 2019”,于是 func 的 await 也被取出,打印“Hello 2019”。

通過上述分析,你發(fā)現(xiàn)了什么現(xiàn)象?那就是 await 與 async 只對調(diào)用上下文的函數(shù)有效,并不向上傳遞。因此對于這個案例而言,func 是在異步等待。如果我們想在 main 函數(shù)中也同步等待,需要在調(diào)用異步函數(shù)時也加上 await,在 main 函數(shù)也加上 async。

各位大佬 應(yīng)該還是迷糊,多看幾遍,多敲幾遍 自然會懂得

總結(jié)

在 UI 編程過程中,異步和多線程是兩個相伴相生的名詞,也是很容易混淆的概念。對于異步方法調(diào)用而言,代碼不需要等待結(jié)果的返回,而是通過其他手段(比如通知、回調(diào)、事件循環(huán)或多線程)在后續(xù)的某個時刻主動(或被動)地接收執(zhí)行結(jié)果。

因此,從辯證關(guān)系上來看,異步與多線程并不是一個同等關(guān)系:異步是目的,多線程只是我們實現(xiàn)異步的一個手段之一。而在 Flutter 中,借助于 UI 框架提供的事件循環(huán),我們可以不用阻塞的同時等待多個異步任務(wù),因此并不需要開多線程。我們一定要記住這一點。

Q/A
  1. 假如有一個任務(wù)(讀寫文件或者網(wǎng)絡(luò))耗時10秒,并且加入到了事件任務(wù)隊列中,執(zhí)行單這個任務(wù)的時候不就把線程卡主嗎?

文件I/O和網(wǎng)絡(luò)調(diào)用并不是在Dart層做的,而是由操作系統(tǒng)提供的異步線程,他倆把活兒干完之后把結(jié)果剛到隊列中,Dart代碼只是執(zhí)行一個簡單的讀動作。

  1. 單線程模型是指的事件隊列模型,和繪制界面的線程是一個嗎
    我們所說的單線程指的是主Isolate。而GPU繪制指令有單獨的線程執(zhí)行,跟主Isolate無關(guān)。事實上Flutter提供了4種task runner,有獨立的線程去運行專屬的任務(wù):
    1.Platform Task Runner:處理來自平臺(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),我們在主Isolate中無需關(guān)心(如果真想主動創(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ù)會返回future,所以用=>的寫法時, 新創(chuàng)建的future會被then返回,第二個then的調(diào)用者就是新的future對象, 所以第二個then就不跟著原先的future 而是跟著它的新future了。
如果把=>換成大括號,此時第二個then還是跟著原先的future,和新future無關(guān)

  1. 下一個奇異的是什么時候

    2045年

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容