文章主題是「單線程模型下如何保證 UI 的流暢性」。該話題針對的是 Flutter 性能原理展開的,可是 dart 語言就是 js 的延伸,不少概念和機制都是同樣的。具體不細聊。此外 js 也是單線程模型,在界面展現和 IO 等方面和 dart 相似。因此結合對比講一下,幫助梳理和類比,更加容易掌握本文的主題,和知識的橫向拓展。css
先從前端角度出發,分析下 event loop 和事件隊列模型。再從 Flutter 層出發聊聊 dart 側的事件隊列和同步異步任務之間的關係。前端
1、單線程模型的設計
1. 最基礎的單線程處理簡單任務
假設有幾個任務:c++
- 任務1: "姓名:" + "杭城小劉"
- 任務2: "年齡:" + "1995" + "02" + "20"
- 任務3: "大小:" + (2021 - 1995 + 1)
- 任務4: 打印任務一、二、3 的結果
在單線程中執行,代碼可能以下:瀏覽器
//c void mainThread () { string name = "姓名:" + "杭城小劉"; string birthday = "年齡:" + "1995" + "02" + "20" int age = 2021 - 1995 + 1; printf("我的信息爲:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age); }
線程開始執行任務,按照需求,單線程依次執行每一個任務,執行完畢後線程立刻退出。安全
2. 線程運行過程當中來了新的任務怎麼處理?
問題1 介紹的線程模型太簡單太理想了,不可能從一開始就 n 個任務就肯定了,大多數狀況下,會接收到新的 m 個任務。那麼 section1 中的設計就沒法知足該需求。 **要在線程運行的過程當中,可以接受並執行新的任務,就須要有一個事件循環機制。**最基礎的事件循環能夠想到用一個循環來實現。網絡
// c++ int getInput() { int input = 0; cout<< "請輸入一個數"; cin>>input; return input; } void mainThread () { while(true) { int input1 = getInput(); int input2 = getInput(); int sum = input1 + input2; print("兩數之和爲:%d", sum); } }
相較於初版線程設計,這一版作了如下改進:數據結構
- 引入了循環機制,線程不會作完事情立刻退出。
- 引入了事件。線程一開始會等待用戶輸入,等待的時候線程處於暫停狀態,當用戶輸入完畢,線程獲得輸入的信息,此時線程被激活。執行相加的操做,最終輸出結果。不斷的等待輸入,並計算輸出。
3. 處理來自其餘線程的任務
真實環境中的線程模塊遠遠沒有這麼簡單。好比瀏覽器環境下,線程可能正在繪製,可能會接收到1個來自用戶鼠標點擊的事件,1個來自網絡加載 css 資源完成的事件等等。第二版線程模型雖然引入了事件循環機制,能夠接受新的事件任務,可是發現沒?這些任務之來自線程內部,該設計是沒法接受來自其餘線程的任務的。多線程
從上圖能夠看出,渲染主線程會頻繁接收到來自於 IO 線程的一些事件任務,當接受到的資源加載完成後的消息,則渲染線程會開始 DOM 解析;當接收到來自鼠標點擊的消息,渲染主線程則會執行綁定好的鼠標點擊事件腳本(js)來處理事件。併發
須要一個合理的數據結構,來存放並獲取其餘線程發送的消息?框架
消息隊列這個詞你們都聽過,在 GUI 系統中,事件隊列是一個通用解決方案。
消息隊列(事件隊列)是一種合理的數據結構。要執行的任務添加到隊列的尾部,須要執行的任務,從隊列的頭部取出。
有了消息隊列以後,線程模型獲得了升級。以下:
能夠看出改造分爲3個步驟:
- 構建一個消息隊列
- IO 線程產生的新任務會被添加到消息隊列的尾部
- 渲染主線程會循環的從消息隊列的頭部讀取任務,執行任務
僞代碼。構造隊列接口部分
class TaskQueue { public: Task fetchTask (); // 從隊列頭部取出1個任務 void addTask (Task task); // 將任務插入到隊列尾部 }
改造主線程
TaskQueue taskQueue; void processTask (); void mainThread () { while (true) { Task task = taskQueue.fetchTask(); processTask(task); } }
IO 線程
void handleIOTask () { Task clickTask; taskQueue.addTask(clickTask); }
Tips: 事件隊列是存在多線程訪問的狀況,因此須要加鎖。
4. 處理來自其餘線程的任務
瀏覽器環境中, 渲染進程常常接收到來自其餘進程的任務,IO 線程專門用來接收來自其餘進程傳遞來的消息。IPC 專門處理跨進程間的通訊。
5. 消息隊列中的任務類型
消息隊列中有不少消息類型。內部消息:如鼠標滾動、點擊、移動、宏任務、微任務、文件讀寫、定時器等等。
消息隊列中還存在大量的與頁面相關的事件。如 JS 執行、DOM 解析、樣式計算、佈局計算、CSS 動畫等等。
上述事件都是在渲染主線程中執行的,所以編碼時需注意,儘可能減少這些事件所佔用的時長。
6. 如何安全退出
Chrome 設計上,肯定要退出當前頁面時,頁面主線程會設置一個退出標誌的變量,每次執行完1個任務時,判斷該標誌。若是設置了,則中斷任務,退出線程
7. 單線程的缺點
事件隊列的特色是先進先出,後進後出。那後進的任務也許會被前面的任務由於執行時間過長而阻塞,等待前面的任務執行完畢才能夠執行後面的任務。這樣存在2個問題。
-
如何處理高優先級的任務
假如要監控 DOM 節點的變化狀況(插入、刪除、修改 innerHTML),而後觸發對應的邏輯。最基礎的作法就是設計一套監聽接口,當 DOM 變化時,渲染引擎同步調用這些接口。不過這樣子存在很大的問題,就是 DOM 變化會很頻繁。若是每次 DOM 變化都觸發對應的 JS 接口,則該任務執行會很長,致使執行效率的下降
若是將這些 DOM 變化作爲異步消息,假如消息隊列中。可能會存在由於前面的任務在執行致使當前的 DOM 消息不會被執行的問題,也就是影響了監控的實時性。
如何權衡效率和實時性?微任務 就是解決該類問題的。
一般,咱們把消息隊列中的任務成爲宏任務,每一個宏任務中都包含一個微任務隊列,在執行宏任務的過程當中,假如 DOM 有變化,則該變化會被添加到該宏任務的微任務隊列中去,這樣子效率問題得以解決。
當宏任務中的主要功能執行完畢歐,渲染引擎會執行微任務隊列中的微任務。所以實時性問題得以解決
-
如何解決單個任務執行時間過長的問題
能夠看出,假如 JS 計算超時致使動畫 paint 超時,會形成卡頓。瀏覽器爲避免該問題,採用 callback 回調的設計來規避,也就是讓 JS 任務延後執行。
2、 flutter 裏的單線程模型
1. event loop 機制
Dart 是單線程的,也就是代碼會有序執行。此外 Dart 做爲 Flutter 這一 GUI 框架的開發語言,必然支持異步。
一個 Flutter 應用包含一個或多個 isolate,默認方法的執行都是在 main isolate 中;一個 isolate 包含1個 Event loop 和1個 Task queue。其中,Task queue 包含1個 Event queue 事件隊列和1個 MicroTask queue 微任務隊列。以下:
爲何須要異步?由於大多數場景下 應用都並非一直在作運算。好比一邊等待用戶的輸入,輸入後再去參與運算。這就是一個 IO 的場景。因此單線程能夠再等待的時候作其餘事情,而當真正須要處理運算的時候,再去處理。所以雖是單線程,可是給咱們的感覺是同事在作不少事情(空閒的時候去作其餘事情)
某個任務涉及 IO 或者異步,則主線程會先去作其餘須要運算的事情,這個動做是靠 event loop 驅動的。和 JS 同樣,dart 中存儲事件任務的角色是事件隊列 event queue。
Event queue 負責存儲須要執行的任務事件,好比 DB 的讀取。
Dart 中存在2個隊列,一個微任務隊列(Microtask Queue)、一個事件隊列(Event Queue)。
Event loop 不斷的輪詢,先判斷微任務隊列是否爲空,從隊列頭部取出須要執行的任務。若是微任務隊列爲空,則判斷事件隊列是否爲空,不爲空則從頭部取出事件(好比鍵盤、IO、網絡事件等),而後在主線程執行其回調函數,以下:
2. 異步任務
微任務,即在一個很短的時間內就會完成的異步任務。微任務在事件循環中優先級最高,只要微任務隊列不爲空,事件循環就不斷執行微任務,後續的事件隊列中的任務持續等待。微任務隊列可由 scheduleMicroTask
建立。
一般狀況,微任務的使用場景比較少。Flutter 內部也在諸如手勢識別、文本輸入、滾動視圖、保存頁面效果等須要高優執行任務的場景用到了微任務。
因此,通常需求下,異步任務咱們使用優先級較低的 Event Queue。好比 IO、繪製、定時器等,都是經過事件隊列驅動主線程來執行的。
Dart 爲 Event Queue 的任務提供了一層封裝,叫作 Future。把一個函數體放入 Future 中,就完成了同步任務到異步任務的包裝(相似於 iOS 中經過 GCD 將一個任務以同步、異步提交給某個隊列)。Future 具有鏈式調用的能力,能夠在異步執行完畢後執行其餘任務(函數)。
看一段具體代碼:
void main() { print('normal task 1'); Future(() => print('Task1 Future 1')); print('normal task 2'); Future(() => print('Task1 Future 2')) .then((value) => print("subTask 1")) .then((value) => print("subTask 2")); } // lbp@MBP ~/Desktop dart index.dart normal task 1 normal task 2 Task1 Future 1 Task1 Future 2 subTask 1 subTask 2
main 方法內,先添加了1個普通同步任務,而後以 Future 的形式添加了1個異步任務,Dart 會將異步任務加入到事件隊列中,而後理解返回。後續代碼繼續以同步任務的方式執行。而後再添加了1個普通同步任務。而後再以 Future 的方式添加了1個異步任務,異步任務被加入到事件隊列中。此時,事件隊列中存在2個異步任務,Dart 在事件隊列頭部取出1個任務以同步的方式執行,所有執行(先進先出)完畢後再執行後續的 then。
Future 與 then 公用1個事件循環。若是存在多個 then,則按照順序執行。
例2:
void main() { Future(() => print('Task1 Future 1')); Future(() => print('Task1 Future 2')); Future(() => print('Task1 Future 3')) .then((_) => print('subTask 1 in Future 3')); Future(() => null).then((_) => print('subTask 1 in empty Future')); } lbp@MBP ~/Desktop dart index.dart Task1 Future 1 Task1 Future 2 Task1 Future 3 subTask 1 in Future 3 subTask 1 in empty Future
main 方法內,Task 1 添加到 Future 1中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 2中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 3中,被 Dart 添加到 Event Queue 中,subTask 1 和 Task 1 共用 Event Queue。Future 4中任務爲空,因此 then 裏的代碼會被加入到 Microtask Queue,以便下一輪事件循環中被執行。
綜合例子
void main() { Future(() => print('Task1 Future 1')); Future fx = Future(() => null); Future(() => print("Task1 Future 3")).then((value) { print("subTask 1 Future 3"); scheduleMicrotask(() => print("Microtask 1")); }).then((value) => print("subTask 3 Future 3")); Future(() => print("Task1 Future 4")) .then((value) => Future(() => print("sub subTask 1 Future 4"))) .then((value) => print("sub subTask 2 Future 4")); Future(() => print("Task1 Future 5")); fx.then((value) => print("Task1 Future 2")); scheduleMicrotask(() => print("Microtask 2")); print("normal Task"); } lbp@MBP ~/Desktop dart index.dart normal Task Microtask 2 Task1 Future 1 Task1 Future 2 Task1 Future 3 subTask 1 Future 3 subTask 3 Future 3 Microtask 1 Task1 Future 4 Task1 Future 5 sub subTask 1 Future 4 sub subTask 2 Future 4
解釋:
- Event Loop 優先執行 main 方法同步任務,再執行微任務,最後執行 Event Queue 的異步任務。因此 normal Task 先執行
- 同理微任務 Microtask 2 執行
- 其次,Event Queue FIFO,Task1 Future 1 被執行
- fx Future 內部爲空,因此 then 裏的內容被加到微任務隊列中去,微任務優先級最高,因此 Task1 Future 2 被執行
- 其次,Task1 Future 3 被執行。因爲存在2個 then,先執行第一個 then 中的 subTask 1 Future 3,而後遇到微任務,因此 Microtask 1 被添加到微任務隊列中去,等待下一次 Event Loop 到來時觸發。接着執行第二個 then 中的 subTask 3 Future 3。隨着下一次 Event Loop 到來,Microtask 1 被執行
- 其次,Task1 Future 4 被執行。隨後的第一個 then 中的任務又是被 Future 包裝成一個異步任務,被添加到 Event Queue 中,第二個 then 中的內容也被添加到 Event Queue 中。
- 接着,執行 Task1 Future 5。本次事件循環結束
- 等下一輪事件循環到來,打印隊列中的 sub subTask 1 Future 四、sub subTask 1 Future 5.
3. 異步函數
異步函數的結果在未來某個時刻才返回,因此須要返回一個 Future 對象,供調用者使用。調用者根據需求,判斷是在 Future 對象上註冊一個 then 等 Future 執行體結束後再進行異步處理,仍是同步等到 Future 執行結束。Future 對象若是須要同步等待,則須要在調用處添加 await,且 Future 所在的函數須要使用 async 關鍵字。
await 並非同步等待,而是異步等待。Event Loop 會將調用體所在的函數也看成異步函數,將等待語句的上下文總體添加到 Event Queue 中,一旦返回,Event Loop 會在 Event Queue 中取出上下文代碼,等待的代碼繼續執行。
await 阻塞的是當前上下文的後續代碼執行,並不能阻塞其調用棧上層的後續代碼執行
void main() { Future(() => print('Task1 Future 1')) .then((_) async => await Future(() => print("subTask 1 Future 2"))) .then((_) => print("subTask 2 Future 2")); Future(() => print('Task1 Future 2')); } lbp@MBP ~/Desktop dart index.dart Task1 Future 1 Task1 Future 2 subTask 1 Future 2 subTask 2 Future 2
解析:
- Future 中的 Task1 Future 1 被添加到 Event Queue 中。其次遇到第一個 then,then 裏面是 Future 包裝的異步任務,因此
Future(() => print("subTask 1 Future 2"))
被添加到 Event Queue 中,所在的 await 函數也被添加到了 Event Queue 中。第二個 then 也被添加到 Event Queue 中 - 第二個 Future 中的 'Task1 Future 2 不會被 await 阻塞,由於 await 是異步等待(添加到 Event Queue)。因此執行 'Task1 Future 2。隨後執行 "subTask 1 Future 2,接着取出 await 執行 subTask 2 Future 2
4. Isolate
Dart 爲了利用多核 CPU,將 CPU 層面的密集型計算進行了隔離設計,提供了多線程機制,即 Isolate。每一個 Isolate 資源隔離,都有本身的 Event Loop 和 Event Queue、Microtask Queue。Isolate 之間的資源共享經過消息機制通訊(和進程同樣)
使用很簡單,建立時須要傳遞一個參數。
void coding(language) { print("hello " + language); } void main() { Isolate.spawn(coding, "Dart"); } lbp@MBP ~/Desktop dart index.dart hello Dart
大多數狀況下,不只僅須要併發執行。可能還須要某個 Isolate 運算結束後將結果告訴主 Isolate。能夠經過 Isolate 的管道(SendPort)實現消息通訊。能夠在主 Isolate 中將管道做爲參數傳遞給子 Isolate,當子 Isolate 運算結束後將結果利用這個管道傳遞給主 Isolate
void coding(SendPort port) { const sum = 1 + 2; // 給調用方發送結果 port.send(sum); } void main() { testIsolate(); } testIsolate() async { ReceivePort receivePort = ReceivePort(); // 建立管道 Isolate isolate = await Isolate.spawn(coding, receivePort.sendPort); // 建立 Isolate,並傳遞發送管道做爲參數 // 監聽消息 receivePort.listen((message) { print("data: $message"); receivePort.close(); isolate?.kill(priority: Isolate.immediate); isolate = null; }); } lbp@MBP ~/Desktop dart index.dart data: 3
此外 Flutter 中提供了執行併發計算任務的快捷方式-compute 函數。其內部對 Isolate 的建立和雙向通訊進行了封裝。
實際上,業務開發中使用 compute 的場景不多,好比 JSON 的編解碼能夠用 compute。
計算階乘:
int testCompute() async { return await compute(syncCalcuateFactorial, 100); } int syncCalcuateFactorial(upperBounds) => upperBounds < 2 ? upperBounds : upperBounds * syncCalcuateFactorial(upperBounds - 1);
總結:
- Dart 是單線程的,但經過事件循環能夠實現異步
- Future 是異步任務的封裝,藉助於 await 與 async,咱們能夠經過事件循環實現非阻塞的同步等待
- Isolate 是 Dart 中的多線程,能夠實現併發,有本身的事件循環與 Queue,獨佔資源。Isolate 之間能夠經過消息機制進行單向通訊,這些傳遞的消息經過對方的事件循環驅動對方進行異步處理。
- flutter 提供了 CPU 密集運算的 compute 方法,內部封裝了 Isolate 和 Isolate 之間的通訊
- 事件隊列、事件循環的概念在 GUI 系統中很是重要,幾乎在前端、Flutter、iOS、Android 甚至是 NodeJS 中都存在。