深入理解Flutter多執行緒

劉小壯發表於2019-04-26
該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/54da18ed1a9e


封面圖

Flutter預設是單執行緒任務處理的,如果不開啟新的執行緒,任務預設在主執行緒中處理。

事件佇列

和iOS應用很像,在Dart的執行緒中也存在事件迴圈和訊息佇列的概念,但在Dart中執行緒叫做isolate。應用程式啟動後,開始執行main函式並執行main isolate

每個isolate包含一個事件迴圈以及兩個事件佇列,event loop事件迴圈,以及event queuemicrotask queue事件佇列,eventmicrotask佇列有點類似iOS的source0source1

  • event queue:負責處理I/O事件、繪製事件、手勢事件、接收其他isolate訊息等外部事件。
  • microtask queue:可以自己向isolate內部新增事件,事件的優先順序比event queue高。

事件佇列

這兩個佇列也是有優先順序的,當isolate開始執行後,會先處理microtask的事件,當microtask佇列中沒有事件後,才會處理event佇列中的事件,並按照這個順序反覆執行。但需要注意的是,當執行microtask事件時,會阻塞event佇列的事件執行,這樣就會導致渲染、手勢響應等event事件響應延時。為了保證渲染和手勢響應,應該儘量將耗時操作放在event佇列中。

async、await

在非同步呼叫中有三個關鍵詞,asyncawaitFuture,其中asyncawait需要一起使用。在Dart中可以通過asyncawait進行非同步操作,async表示開啟一個非同步操作,也可以返回一個Future結果。如果沒有返回值,則預設返回一個返回值為nullFuture

asyncawait本質上就是Dart對非同步操作的一個語法糖,可以減少非同步呼叫的巢狀呼叫,並且由async修飾後返回一個Future,外界可以以鏈式呼叫的方式呼叫。這個語法是JSES7標準中推出的,Dart的設計和JS相同。

下面封裝了一個網路請求的非同步操作,並且將請求後的Response型別的Future返回給外界,外界可以通過await呼叫這個請求,並獲取返回資料。從程式碼中可以看到,即便直接返回一個字串,Dart也會對其進行包裝併成為一個Future

Future<Response> dataReqeust() async {
    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
    Client client = Client();
    Future<Response> response = client.get(requestURL);
    return response;
}

Future<String> loadData() async {
    Response response = await dataReqeust();
    return response.body;
}
複製程式碼

在程式碼示例中,執行到loadData方法時,會同步進入方法內部進行執行,當執行到await時就會停止async內部的執行,從而繼續執行外面的程式碼。當await有返回後,會繼續從await的位置繼續執行。所以await的操作,不會影響後面程式碼的執行。

下面是一個程式碼示例,通過async開啟一個非同步操作,通過await等待請求或其他操作的執行,並接收返回值。當資料發生改變時,呼叫setState方法並更新資料來源,Flutter會更新對應的Widget節點檢視。

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}
複製程式碼

Future

Future就是延時操作的一個封裝,可以將非同步任務封裝為Future物件。獲取到Future物件後,最簡單的方法就是用await修飾,並等待返回結果繼續向下執行。正如上面async、await中講到的,使用await修飾時需要配合async一起使用。

Dart中,和時間相關的操作基本都和Future有關,例如延時操作、非同步操作等。下面是一個很簡單的延時操作,通過Futuredelayed方法實現。

loadData() {
    // DateTime.now(),獲取當前時間
    DateTime now = DateTime.now();
    print('request begin $now');
    Future.delayed(Duration(seconds: 1), (){
      now = DateTime.now();
      print('request response $now');
    });
}
複製程式碼

Dart還支援對Future的鏈式呼叫,通過追加一個或多個then方法來實現,這個特性非常實用。例如一個延時操作完成後,會呼叫then方法,並且可以傳遞一個引數給then。呼叫方式是鏈式呼叫,也就代表可以進行很多層的處理。這有點類似於iOS的RAC框架,鏈式呼叫進行訊號處理。

Future.delayed(Duration(seconds: 1), (){
  int age = 18;
  return age;
}).then((onValue){
  onValue++;
  print('age $onValue');
});
複製程式碼

協程

如果想要了解asyncawait的原理,就要先了解協程的概念,asyncawait本質上就是協程的一種語法糖。協程,也叫作coroutine,是一種比執行緒更小的單元。如果從單元大小來說,基本可以理解為程式->執行緒->協程。

任務排程

在弄懂協程之前,首先要明白併發和並行的概念,併發指的是由系統來管理多個IO的切換,並交由CPU去處理。並行指的是多核CPU在同一時間裡執行多個任務。

併發的實現由非阻塞操作+事件通知來完成,事件通知也叫做“中斷”。操作過程分為兩種,一種是CPU對IO進行操作,在操作完成後發起中斷告訴IO操作完成。另一種是IO發起中斷,告訴CPU可以進行操作。

執行緒本質上也是依賴於中斷來進行排程的,執行緒還有一種叫做“阻塞式中斷”,就是在執行IO操作時將執行緒阻塞,等待執行完成後再繼續執行。但執行緒的消耗是很大的,並不適合大量併發操作的處理,而通過單執行緒併發可以進行大量併發操作。當多核CPU出現後,單個執行緒就無法很好的利用多核CPU的優勢了,所以又引入了執行緒池的概念,通過執行緒池來管理大量執行緒。

協程

在程式執行過程中,離開當前的呼叫位置有兩種方式,繼續呼叫其他函式和return返回離開當前函式。但是執行return時,當前函式在呼叫棧中的區域性變數、形參等狀態則會被銷燬。

協程分為無線協程和有線協程,無線協程在離開當前呼叫位置時,會將當前變數放在堆區,當再次回到當前位置時,還會繼續從堆區中獲取到變數。所以,一般在執行當前函式時就會將變數直接分配到堆區,而asyncawait就屬於無線協程的一種。有線協程則會將變數繼續儲存在棧區,在回到指標指向的離開位置時,會繼續從棧中取出呼叫。

async、await原理

asyncawait為例,協程在執行時,執行到async則表示進入一個協程,會同步執行async的程式碼塊。async的程式碼塊本質上也相當於一個函式,並且有自己的上下文環境。當執行到await時,則表示有任務需要等待,CPU則去排程執行其他IO,也就是後面的程式碼或其他協程程式碼。過一段時間CPU就會輪訓一次,看某個協程是否任務已經處理完成,有返回結果可以被繼續執行,如果可以被繼續執行的話,則會沿著上次離開時指標指向的位置繼續執行,也就是await標誌的位置。

由於並沒有開啟新的執行緒,只是進行IO中斷改變CPU排程,所以網路請求這樣的非同步操作可以使用asyncawait,但如果是執行大量耗時同步操作的話,應該使用isolate開闢新的執行緒去執行。

如果用協程和iOS的dispatch_async進行對比,可以發現二者是比較相似的。從結構定義來看,協程需要將當前await的程式碼塊相關的變數進行儲存,dispatch_async也可以通過block來實現臨時變數的儲存能力。

我之前還在想一個問題,蘋果為什麼不引入協程的特性呢?後來想了一下,awaitdispatch_async都可以簡單理解為非同步操作,OC的執行緒是基於Runloop實現的,Dart本質上也是有事件迴圈的,而且二者都有自己的事件佇列,只是佇列數量和分類不同。

我覺得當執行到await時,儲存當前的上下文,並將當前位置標記為待處理任務,用一個指標指向當前位置,並將待處理任務放入當前isolate的佇列中。在每個事件迴圈時都去詢問這個任務,如果需要進行處理,就恢復上下文進行任務處理。

Promise

這裡想提一下JS裡的Promise語法,在iOS中會出現很多if判斷或者其他的巢狀呼叫,而Promise可以把之前橫向的巢狀呼叫,改成縱向鏈式呼叫。如果能把Promise引入到OC裡,可以讓程式碼看起來更簡潔,直觀。

isolate

isolateDart平臺對執行緒的實現方案,但和普通Thread不同的是,isolate擁有獨立的記憶體,isolate由執行緒和獨立記憶體構成。正是由於isolate執行緒之間的記憶體不共享,所以isolate執行緒之間並不存在資源搶奪的問題,所以也不需要鎖。

通過isolate可以很好的利用多核CPU,來進行大量耗時任務的處理。isolate執行緒之間的通訊主要通過port來進行,這個port訊息傳遞的過程是非同步的。通過Dart原始碼也可以看出,例項化一個isolate的過程包括,例項化isolate結構體、在堆中分配執行緒記憶體、配置port等過程。

isolate看起來其實和程式比較相似,之前請教阿里架構師宗心問題時,宗心也說過“isolate的整體模型我自己的理解其實更像程式,而asyncawait更像是執行緒”。如果對比一下isolate和程式的定義,會發現確實isolate很像是程式。

程式碼示例

下面是一個isolate的例子,例子中新建立了一個isolate,並且繫結了一個方法進行網路請求和資料解析的處理,並通過port將處理好的資料返回給呼叫方。

loadData() async {
    // 通過spawn新建一個isolate,並繫結靜態方法
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    
    // 獲取新isolate的監聽port
    SendPort sendPort = await receivePort.first;
    // 呼叫sendReceive自定義方法
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
}

// isolate的繫結方法
static dataLoader(SendPort sendPort) async{
    // 建立監聽port,並將sendPort傳給外界用來呼叫
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);
    
    // 監聽外界呼叫
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];
    
      Client client = Client();
      Response response = await client.get(requestURL);
      List dataList = json.decode(response.body);
      // 回撥返回值給呼叫者
      callbackPort.send(dataList);
    }    
}

// 建立自己的監聽port,並且向新isolate傳送訊息
Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // 接收到返回值,返回給呼叫者
    return receivePort.first;
}
複製程式碼

isolate和iOS中的執行緒還不太一樣,isolate的執行緒更偏底層。當生成一個isolate後,其記憶體是各自獨立的,相互之間並不能進行訪問。但isolate提供了基於port的訊息機制,通過建立通訊雙方的sendPortreceiveport,進行相互的訊息傳遞,在Dart中叫做訊息傳遞。

從上面例子中可以看出,在進行isolate訊息傳遞的過程中,本質上就是進行port的傳遞。將port傳遞給其他isolate,其他isolate通過port拿到sendPort,向呼叫方傳送訊息來進行相互的訊息傳遞。

Embedder

正如其名,Embedder是一個嵌入層,將Flutter嵌入到各個平臺上。Embedder負責範圍包括原生平臺外掛、執行緒管理、事件迴圈等。

Flutter System Overriew

Embedder中存在四個Runner,四個Runner分別如下。其中每個Flutter Engine各自對應一個UI RunnerGPU RunnerIO Runner,但所有Engine共享一個Platform Runner

Embedder

Runnerisolate並不是一碼事,彼此相互獨立。以iOS平臺為例,Runner的實現就是CFRunLoop,以一個事件迴圈的方式不斷處理任務。並且Runner不只處理Engine的任務,還有Native Plugin帶來的原生平臺的任務。而isolate則由Dart VM進行管理,和原生平臺執行緒並無關係。

Platform Runner

Platform Runner和iOS平臺的Main Thread非常相似,在Flutter中除耗時操作外,所有任務都應該放在Platform中,Flutter中的很多API並不是執行緒安全的,放在其他執行緒中可能會導致一些bug。

但例如IO之類的耗時操作,應該放在其他執行緒中完成,否則會影響Platform的正常執行,甚至於被watchdog幹掉。但需要注意的是,由於Embedder Runner的機制,Platform被阻塞後並不會導致頁面卡頓。

不只是Flutter Engine的程式碼在Platform中執行,Native Plugin的任務也會派發到Platform中執行。實際上,在原生側的程式碼執行在Platform Runner中,而Flutter側的程式碼執行在Root Isolate中,如果在Platform中執行耗時程式碼,則會卡原生平臺的主執行緒。

UI Runner

UI Runner負責為Flutter Engine執行Root Isolate的程式碼,除此之外,也處理來自Native Plugin的任務。Root Isolate為了處理自身事件,繫結了很多函式方法。程式啟動時,Flutter Engine會為Root繫結UI Runner的處理函式,使Root Isolate具備提交渲染幀的能力。

Root IsolateEngine提交一次渲染幀時,Engine會等待下次vsync,當下次vsync到來時,由Root IsolateWidgets進行佈局操作,並生成頁面的顯示資訊的描述,並將資訊交給Engine去處理。

由於對widgets進行layout並生成layer treeUI Runner進行的,如果在UI Runner中進行大量耗時處理,會影響頁面的顯示,所以應該將耗時操作交給其他isolate處理,例如來自Native Plugin的事件。

Rendering Pipeline.jpg

GPU Runner

GPU Runner並不直接負責渲染操作,其負責GPU相關的管理和排程。當layer tree資訊到來時,GPU Runner將其提交給指定的渲染平臺,渲染平臺是Skia配置的,不同平臺可能有不同的實現。

GPU Runner相對比較獨立,除了Embedder外其他執行緒均不可向其提交渲染資訊。

Graphics Pipeline

IO Runner

一些GPU Runner中比較耗時的操作,就放在IO Runner中進行處理,例如圖片讀取、解壓、渲染等操作。但是隻有GPU Runner才能對GPU提交渲染資訊,為了保證IO Runner也具備這個能力,所以IO Runner會引用GPU Runnercontext,這樣就具備向GPU提交渲染資訊的能力。

相關文章