## Flutter ## 單/多執行緒之謎? --- <!-- .slide: data-background="#000000" --> # <font color="#f00">友情警告</font> 接下來你可能會聽到一系列不正確的英文發音, 請自行考證正確發音,切勿直接對外人使用! 還有以下的所有 Example Code 皆是概念, 請勿直接複製使用,將會無法正常運行! --- ### 是 Multi-Thread?<br>還是Single-Thread? ###### 答案是 Single-Thread<!-- .element: class="fragment" data-fragment-index="0" --> ###### 但 Multi-Thread 可以做到的,它也可以!<!-- .element: class="fragment" data-fragment-index="1" --> Note: 沒有 Thread 但有 Isolate --- ## 現在我們先思考一下 #### Single Thread 會造成什麼問題? 無法有效利用多核 CPU<!-- .element: class="fragment" data-fragment-index="0" --> 會產生 Blocking 問題<!-- .element: class="fragment" data-fragment-index="2" --> --- #### 講到這邊你有沒有想起誰? #### 前端唯一霸主 JavaScript<!-- .element: class="fragment" data-fragment-index="0" --> <br> #### JS 也是 Single-Thread<!-- .element: class="fragment" data-fragment-index="1" --> #### 但似乎沒有 Blocking 的問題?<!-- .element: class="fragment" data-fragment-index="1" --> Note: 雖然 Flutter 支援 Web 平台,但實作方式是把 Dart 編譯成 Javascript ---- #### 因為 JS 有一個東西叫做 Promise #### Flutter 也有一個叫做 Future<!-- .element: class="fragment" data-fragment-index="0" --> <br> 這兩個是幾乎一樣的東西...<!-- .element: class="fragment" data-fragment-index="1" --> Note: 在沒有 Promise 的年代靠 callback callback -> promise -> async/await(語法糖) ---- #### 我先來看一下簡單的 Future 範例 ``` dart main() { printOne(); printTwo(); } printOne() { print(1); } printTwo() { future((){ print(3); }) print(2); } ``` #### Output <!-- .element: class="fragment" data-fragment-index="0" --> ``` dart 1 2 3 ``` <!-- .element: class="fragment" data-fragment-index="0" --> ---- ##### Async / Await Flutter 一樣提供了這兩個語法糖 讓你快速把 function 包裝成 Future ``` dart getTestString() async { return 'is test' } main() { String text = getTestString(); } ``` <!-- .element: class="fragment" data-fragment-index="1" --> ``` dart String <= getTestString() {} Future<String> <= getTestString() async {} ``` <!-- .element: class="fragment" data-fragment-index="2" --> --- ## 那麼問題來了 #### 明明是單執行緒語言,那為什麼可以異步執行? <br> ## Event Loop<!-- .element: class="fragment" data-fragment-index="0" --> 一個讓單執行緒語言可以避免阻塞的機制<!-- .element: class="fragment" data-fragment-index="1" --> ---- ![](https://miro.medium.com/max/1400/1*Q5G85mEiMfPE5Y3dqQqPEg.png) Note: 這張圖是 Flutter 的,JS 架構有些不一樣。 JS 流程是每執行一個 Task 後會去執行所有 MicroTask 最後才渲染。 Flutter 因為實作了這套機制,所以大部分情況下都可以用單執行緒完成運算,所以才被定義成單執行緒語言。 ---- ## Event Loop 並非萬能! 雖然透過異步呼叫的方式來執行, 但如果運算太耗時一樣會造成 Blocking <!-- .element: class="fragment" data-fragment-index="0" --> ---- #### 現在我們先假定你有一台非常爛的電腦 #### 運算 SUM(1000) 需要三秒這麼久 ---- #### 然後讓我們來看一下這範例 ``` javascript sum(n) { result = 0; for(i = 1; i <= n; i++) { result += i; } return result; } main() { print(1); print(sum(1000)); print(2); } ``` ``` dart 1 等待三秒後... 500500 2 ``` <!-- .element: class="fragment" data-fragment-index="0" --> ---- #### 所以我們會這樣改 ``` javascript sum(n) async { result = 0; for(i = 1; i <= n; i++) { result += i; } return result; } main() { print(1); sum(1000).then(s => print(s)); print(2); } ``` ``` output 1 2 等待三秒後... 500500 ``` <!-- .element: class="fragment" data-fragment-index="0" --> ## <font color="#f00">沒有!!!</font><!-- .element: class="fragment" data-fragment-index="1" --> ---- ### 事實上問題並沒有被解決 透過異步呼叫也僅僅是改變了系統的執行順序, 運算本身還是消耗了三秒。 <br> 這三秒之間系統將無法執行其他事情 用戶的 UI 互動或事件執行將被<font color="#f00">`阻塞`</font> <!-- .element: class="fragment" data-fragment-index="0" --> ---- #### 所以我們必須做任務拆分 ``` javascript splitSum(n, m) async { result = 0; for(i = n; i <= m; i++) { result += i; } return result; } ``` ``` javascript sum(n) async { result = 0; for (i = 1; i <= (n / 100).ceil(); i++){ start = 1 + (100 * (i -1)); end = 100 * i > n ? n : 100 * i; result += await splitSum(start, end); } return result; } ``` ---- ### 但事實上並沒有這麼簡單 有些運算是無法拆分的<!-- .element: class="fragment" data-fragment-index="0" --> 就算能拆分也存在一定的效能問題<!-- .element: class="fragment" data-fragment-index="1" --> --- ### 書到用時方恨少, ### 事非經過不知難。 Note: 剛開始因為 Event Loop 嚐到甜頭,但越後面效能越來越吃重,才發現光有異步呼叫是不夠的! ---- #### 現在 Event Loop 已經滿足不了我了, #### 但是 Flutter 並沒有提供 Thread 怎麼辦? <br> ## <span>Flutter 提供了 <font color="#ff0000">Isolate</font></span><!-- .element: class="fragment" data-fragment-index="0" --> ---- ## 什麼是 Isolate ? 一個重新包裝過的 Thread<!-- .element: class="fragment" data-fragment-index="0" --> 一個 Actor Model 的實作<!-- .element: class="fragment" data-fragment-index="1" --> Note: Actor Model 是一種多線程的模型,除此之外還有其他模型 CSP, STM, Guilds.... ---- #### 講 Isolate 前,我們先聊聊 Actor Model ![](https://static.coderbridge.com/img/techbridge/images/arvinh/actor-model.png) <!-- .element: class="fragment" data-fragment-index="0" --> Note: Actor Model 有幾個特點: 1. 一個 Actor 只專注一件事。 2. Actor 有自己的狀態,不與其他人共享。 3. 所有溝通都透過 Message 進行 ---- ### 在這樣的架構下 ### 每個 Actor 只能做三件事情 1. 接收 Message 並處理<!-- .element: class="fragment" data-fragment-index="0" --> 2. 傳送 Message<!-- .element: class="fragment" data-fragment-index="1" --> 3. 建立新的 Actor<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 這樣設計有什麼好處? <span>1. 不會產生競爭條件</span><!-- .element: class="fragment" data-fragment-index="0" --><span>,所以不需要奇形怪狀的 Lock</span><!-- .element: class="fragment" data-fragment-index="1" --> <span>2. 每個 Actor 之間不互相依賴</span><!-- .element: class="fragment" data-fragment-index="2" --><span>,沒有執行順序問題</span><!-- .element: class="fragment" data-fragment-index="3" --> <span>3. 天生的分散式系統</span><!-- .element: class="fragment" data-fragment-index="4" --><span>,容易擴展並且本地遠程通吃</span><!-- .element: class="fragment" data-fragment-index="5" --> Note: Flutter 的 Isolate 並未支援遠程 Actor ---- ### Actor Model 就這麼完美嗎? #### 並非如此!<!-- .element: class="fragment" data-fragment-index="0" --> 記憶體消耗 <!-- .element: class="fragment" data-fragment-index="1" --> 資料的一致性 <!-- .element: class="fragment" data-fragment-index="2" --> 多個 Actor 之間溝通複雜性 <!-- .element: class="fragment" data-fragment-index="3" --> Note: 1.記憶體不共享,所以傳送資料會以副本的方式進行,導致同一份資料會佔用多份記憶體 2.當有多個資料副本時,就會產生資料一致性問題 3.如果服務有多個 Actor 透過 Message 將會很複雜 --- #### 講完 Actor 後,我們再來聊點 Isolate 在 Flutter 裡本身 Main() 主程式就是一個 Isolate <!-- .element: class="fragment" data-fragment-index="0" --> 而每個 Isolate 都只會由一個 Thead 負責 <!-- .element: class="fragment" data-fragment-index="1" --> Note: 這也是 Flutter 被稱為單執行緒語言的原因之一 ---- #### 想使用 Isolate 前,你必須先這知道三樣東西 * Isolate * ReceivePort * SendPort Note: Isolate 本身就是負責運算 method ReceivePort 及 SendPort 是一對的,負責傳送訊息溝通 ---- #### 接下來我們來了解如何建立 Isolate #### 方法有以下兩種 <font color="#ff0000">Spawn<!-- .element: class="fragment" data-fragment-index="0" --></font><span> 與 <!-- .element: class="fragment" data-fragment-index="1" --></span><font color="#ff0000">SpawnUrl<!-- .element: class="fragment" data-fragment-index="1" --></font> ---- #### 我們先來看 Spawn ``` javascript sum(Array data) { SendPort sendPort = data[0]; result = 0; for(i = 1; i <= data[1]; i++) { result += i; } sendPort.send(result); } ``` ``` javascript main() { print(1); ReceivePort receivePort = ReceivePort(); SendPort sendPort = receivePort.sendPort isolate = await Isolate.spawn(sum, [sendPort, 1000]); receivePort.listen((result) { print(result); }); print(2); } ``` ---- #### 以為這樣就結束了嗎?? #### 沒有!!!<!-- .element: class="fragment" data-fragment-index="0" --> 你還要自己 Kill Isolate<!-- .element: class="fragment" data-fragment-index="1" --> ``` javascript isolate.kill(priority: Isolate.immediate); ``` <!-- .element: class="fragment" data-fragment-index="1" --> Note: Kill 優先級分為 immediate 立即刪除 BeforeNextEvent 下次循環 Event 時刪除 ---- #### 可是你說我只是運算一次而已, #### 每次還要建立跟管理一堆 ReceivePort 與 SendPort, <!-- .element: class="fragment" data-fragment-index="1" --> #### 還要自己 Kill 也太麻煩了吧?<!-- .element: class="fragment" data-fragment-index="2" --> ---- #### 你的聲音 Flutter 聽到了 #### `Compute`重新包裝了 Isolate<!-- .element: class="fragment" data-fragment-index="1" --> 現在你只要專注與 Isolate 需要運算什麼就好了<!-- .element: class="fragment" data-fragment-index="2" --> ---- #### 按照慣例我們在搬出那台很爛的電腦 ``` javascript sum(n) { result = 0; for(i = 1; i <= n; i++) { result += i; } return n; } ``` ``` javascript main() { print(1); compute(sum, 1000).then((result) { print(result) }); print(2) } ``` Note: Compute 是利用 async 去重新包裝了 Isolate, 當你呼叫 Compute 時,他會去建立一個 Isolate 再把你的 method 丟進去,然後用一個 Future 包裝起來,等 Isolate 回應後就自動 Kill 掉,在執行 then ---- ### Compute 簡化了 Isolate 溝通成本 <br> 但也失去互相溝通的機制, 所以只適合拿來用在單次運算的地方 <!-- .element: class="fragment" data-fragment-index="1" --> Note: Comput 適合單次運算,例如加解密 Isolate 適合持續性的運算,例如 LogIsolate 一直收集 Log 並記錄 --- ## Future 及 Isolate 使用時機 <br> 基本上大部分的情況都可以使用 Future 解決,<!-- .element: class="fragment" data-fragment-index="0" --> 因為前端性質的服務很少會有耗時的操作,<!-- .element: class="fragment" data-fragment-index="1" --> 大部分耗時操作都是由外部 API 幫你執行,<!-- .element: class="fragment" data-fragment-index="2" --> 所以只有當你有很奇耙的運算, 才需要搬出 Isolate 來坐鎮<!-- .element: class="fragment" data-fragment-index="3" --> Note: 這也是為什麼 Flutter 使用 Event Loop 機制,而不是直接使用多執行緒機制的原因之一 --- #### 這次主要是分享一些單多執行緒的差異 #### 所以前面很多提到的都只是觀念跟運作方式 #### 事實上在使用的時候還有很多眉角跟限制 #### 不過不在這次討論內容,有興趣的可以自行查閱 <br> #### 最後感謝大家的收聽 <div style="font-size: 12px"> 以上圖片及資料都來自網路資料 </div>
{"metaMigratedAt":"2023-06-15T05:52:54.715Z","metaMigratedFrom":"YAML","title":"Flutter 單/多執行緒之謎","breaks":true,"slideOptions":"{\"progress\":true,\"theme\":\"solarized\",\"transition\":\"concave\",\"spotlight\":{\"enabled\":true}}","contributors":"[{\"id\":\"6cafde5d-0be9-4081-bb7a-2b953ef414d1\",\"add\":16215,\"del\":6574}]"}
    2198 views