Flutter中Dart非同步模型

xiaopeng發表於2019-11-15

前言

我們知道Flutter 框架有出色的渲染和互動能力。支撐起這些複雜的能力背後,實際上是基於單執行緒模型的 Dart。那麼,與原生 Android 和 iOS 的多執行緒機制相比,單執行緒的 Dart 如何從語言設計層面和程式碼執行機制上保證 Flutter UI 的流暢性呢?

Flutter中Dart非同步模型
單執行緒模型

我們從下面幾個方面闡述一下:

  1. Dart 語言單執行緒模型和 Event Loop 處理機制
  2. 非同步處理和併發程式設計的原理和使用方法
  3. Dart 單執行緒模型下的程式碼執行本質

1. Dart單執行緒模型

dart是單執行緒執行的。怎麼理解這句話呢, 從下面幾個方面可以看到這個設計思想.

1.1 預設單一執行的執行緒

dart預設執行在Main函式存線上程,在dart中稱之為isolate,這個執行緒我們可稱之為main isolate。單執行緒任務處理的,如果不開啟新的isolate,任務預設在主isolate中處理。一旦 Dart 函式執行,它將按照在 main 函式出現的次序一個接一個地持續執行,直到退出。換而言之,Dart 函式在執行期間,無法被其他 Dart 程式碼打斷。

1.2 獨享記憶體

Android和IOS可以自由的開闢除了UI主執行緒之外的執行緒,這些執行緒和主執行緒可以共享記憶體的變數,但是, Dart中的isolate無法共享記憶體。Isolate 不能共享記憶體,他們就像是單獨的分離的 app,通過訊息進行溝通。除了顯式指定程式碼執行在別的 isolate 或者 worker 中,其他程式碼都執行在 app 的 main isolate 中。更多資訊可以訪問Use isolates or workers if necessary

1.3 質疑

(1)假如有一個任務(讀寫檔案或者網路)耗時10秒,並且加入到了事件任務佇列中,執行單這個任務的時候不就把執行緒卡主嗎?

答:檔案I/O和網路呼叫並不是在Dart層做的,而是由作業系統提供的非同步執行緒,他倆把活兒幹完之後把結果剛到佇列中,Dart程式碼只是執行一個簡單的讀動作。

(2)單執行緒模型是指的事件佇列模型,和繪製介面的執行緒是一個嗎?

答:我們所說的單執行緒指的是主Isolate。而GPU繪製指令有單獨的執行緒執行,跟主Isolate無關。事實上Flutter提供了4種task runner,有獨立的執行緒去執行專屬的任務:參見:深入理解Flutter引擎執行緒模式

  1. Platform Task Runner:處理來自平臺(Android/iOS)的訊息
  2. UI Task Runner:執行渲染邏輯、處理native plugin的訊息、timer、microtask、非同步I/O操作處理等
  3. GPU Task Runner:執行GPU指令
  4. IO Task Runner:執行I/O任務

2. Event Loop 機制

Flutter中Dart非同步模型
訊息佇列模型

如圖所示,dart也存在事件佇列和事件迴圈。每個isolate也包含一個事件迴圈,區別是他有兩個事件佇列,event loop事件迴圈,以及event queue和microtask queue事件佇列,event和microtask佇列有點類似iOS的source0和source1。

  • event queue:負責處理I/O事件、繪製事件、手勢事件、接收其他isolate訊息等外部事件。
  • microtask queue:可以自己向isolate內部新增事件,事件的優先順序比event queue高。
Flutter中Dart非同步模型
事件佇列模型
  1. 先檢查MicroTask佇列是否為空,非空則先執行MicroTask佇列中的MicroTask
  2. 一個MicroTask執行完後,檢查有沒有下一個MicroTask,直到MicroTask佇列為空,才去執行Event佇列
  3. Evnet 佇列取出一個事件處理完後,再次返回第一步,去檢查MicroTask佇列是否為空

我們可以看出,將任務加入到MicroTask中可以被儘快執行,但也需要注意,當事件迴圈在處理MicroTask佇列時,會阻塞event佇列的事件執行,這樣就會導致渲染、手勢響應等event事件響應延時。為了保證渲染和手勢響應,應該儘量將耗時操作放在event佇列中。

我們通常很少會直接用到微任務佇列,就連 Flutter 內部,也只有 7 處用到了而已(比如,手勢識別、文字輸入、滾動檢視、儲存頁面效果等需要高優執行任務的場景)。

簡單總結為一二一模型:1個事件迴圈和2個佇列的單執行緒執行模型。

3. 非同步任務排程

為什麼單執行緒也可以非同步?這裡有一個大前提,那就是我們的 App 絕大多數時間都在等待。比如,等使用者點選、等網路請求返回、等檔案 IO 結果,等等。而這些等待行為並不是阻塞的。比如說,網路請求,Socket 本身提供了 select 模型可以非同步查詢;而檔案 IO,作業系統也提供了基於事件的回撥機制。所以,基於這些特點,單執行緒模型可以在等待的過程中做別的事情,等真正需要響應結果了,再去做對應的處理。因為等待過程並不是阻塞的,所以給我們的感覺就像是同時在做多件事情一樣。但其實始終只有一個執行緒在處理你的事情。

非同步任務我們用的最多的還是優先順序更低的 Event Queue。比如,I/O、繪製、定時器這些非同步事件,都是通過事件佇列驅動主執行緒執行的。

3.1 用Future發起非同步任務

Dart 為 Event Queue 的任務建立提供了一層封裝,叫作 Future。Future 還提供了鏈式呼叫的能力,可以在非同步任務執行完畢後依次執行鏈路上的其他函式體。

new Future((){
    //  doing something
});複製程式碼

微任務是由 scheduleMicroTask 建立的。如下所示,這段程式碼會在下一個事件迴圈中輸出一段字串:

scheduleMicrotask(() => print('This is a microtask'));複製程式碼

鏈式呼叫:

Future(() => print('Running in Future 1'));//下一個事件迴圈輸出字串

Future(() => print(‘Running in Future 2'))
  .then((_) => print('and then 1'))
  .then((_) => print('and then 2’));//上一個事件迴圈結束後,連續輸出三段字串複製程式碼

Dart 會將非同步任務的函式執行體放入事件佇列,然後立即返回,後續的程式碼繼續同步執行。而當同步執行的程式碼執行完畢後,事件佇列會按照加入事件佇列的順序(即宣告順序),依次取出事件,最後同步執行 Future 的函式體及後續的 then。這意味著,then 與 Future 函式體共用一個事件迴圈。而如果 Future 有多個 then,它們也會按照鏈式呼叫的先後順序同步執行,同樣也會共用一個事件迴圈。

如果 Future 執行體已經執行完畢了,但你又拿著這個 Future 的引用,往裡面加了一個 then 方法體,這時 Dart 會如何處理呢?面對這種情況,Dart 會將後續加入的 then 方法體放入微任務佇列,儘快執行。

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

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

//then 4會加入微任務佇列,儘快執行
Future(() => null).then((_) => print('then 4'));
結果: f1 f2 f3 then 3 then 4複製程式碼

4. 非同步函式

Future 是非同步任務的封裝,藉助於 await 與 async,我們可以通過事件迴圈實現非阻塞的同步等待。Dart 中的 await 並不是阻塞等待,而是非同步等待。Dart 會將呼叫體的函式也視作非同步函式,將等待語句的上下文放入 Event Queue 中,一旦有了結果,Event Loop 就會把它從 Event Queue 中取出,等待程式碼繼續執行。

async關鍵字作為方法宣告的字尾時,具有如下意義

  • 被修飾的方法會將一個 Future 物件作為返回值
  • 該方法會同步執行其中的方法的程式碼直到第一個 await 關鍵字,然後它暫停該方法其他部分的執行;
  • 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行程式碼將立即執行。
// 匯入io庫,呼叫sleep函式
import 'dart:io';

// 模擬耗時操作,呼叫sleep函式睡眠2秒
doTask() async{
  await sleep(const Duration(seconds:2));
  return "Ok";
}

// 定義一個函式用於包裝
test() async {
  var r = await doTask();
  print(r);
}

void main(){
  print("main start");
  test();
  print("main end");
}
結果:
main start
main end
Ok複製程式碼

我們先來看下這段程式碼。第二行的 then 執行體 f2 是一個 Future,為了等它完成再進行下一步操作,我們使用了 await,期望列印結果為 f1、f2、f3、f4:

Future(()=>print('f1'))
.then((_)async=>awaitFuture(()=>print('f2')))
.then((_)=>print('f3'));
Future(()=>print('f4'));複製程式碼

實際上,當你執行這段程式碼時就會發現,列印出來的結果其實是 f1、f4、f2、f3!

  • 分析一下這段程式碼的執行順序:
  • 按照任務的宣告順序,f1 和 f4 被先後加入事件佇列。
  • f1 被取出並列印;
  • 然後到了 then。then 的執行體是個 future f2,於是放入 Event Queue。
  • 然後把 await 也放到 Event Queue 裡。這個時候要注意了,Event Queue 裡面還有一個 f4,我們的 await 並不能阻塞 f4 的執行。因此,Event Loop 先取出 f4,列印 f4;
  • 然後才能取出並列印 f2,最後把等待的 await 取出,開始執行後面的 f3。

由於 await 是採用事件佇列的機制實現等待行為的,所以比它先在事件佇列中的 f4 並不會被它阻塞。

5. Isolate

Dart 也提供了多執行緒機制,即 Isolate(這個單詞的中文意思是隔離)。在 Isolate 中,資源隔離做得非常好,每個 Isolate 都有自己的 Event Loop 與 Queue,Isolate 之間不共享任何資源,只能依靠訊息機制通訊,因此也就沒有資源搶佔問題。如下所示,我們宣告瞭一個 Isolate 的入口函式,然後在 main 函式中啟動它,並傳入了一個字串引數:

doSth(msg) => print(msg);

main() {
  Isolate.spawn(doSth, "Hi");
  ...
}複製程式碼

那麼如何利用訊息機制進行通訊呢,下面引用了一篇文章的講解,圖畫的很好。

Flutter中Dart非同步模型
引用

整個訊息通訊過程如上圖所示,兩個Isolate是通過兩對Port物件通訊,一對Port分別由用於接收訊息的ReceivePort物件,和用於傳送訊息的SendPort物件構成。其中SendPort物件不用單獨建立,它已經包含在ReceivePort物件之中。需要注意,一對Port物件只能單向發訊息,這就如同一根自來水管,ReceivePortSendPort分別位於水管的兩頭,水流只能從SendPort這頭流向ReceivePort這頭。因此,兩個Isolate之間的訊息通訊肯定是需要兩根這樣的水管的,這就需要兩對Port物件。

6. 引用文章

(1)23 | 單執行緒模型怎麼保證UI執行流暢?

(2)Dart 非同步程式設計詳解之一文全懂

(3)Dart asynchronous programming: Isolates and event loops


相關文章