Dart 語言非同步編程之Isolate
- 2019 年 10 月 4 日
- 筆記
- 非同步編程之Isolate
- spawnUri
- spawn
- Flutter 中創建Isolate
- 使用場景
非同步編程之Isolate
之前的文章已經說過,將非常耗時的任務添加到事件隊列後,仍然會拖慢整個事件循環的處理,甚至是阻塞。可見基於事件循環的非同步模型仍然是有很大缺點的,這時候我們就需要Isolate
,這個單詞的中文意思是隔離。
簡單說,可以把它理解為Dart中的執行緒。但它又不同於執行緒,更恰當的說應該是微執行緒,或者說是協程。它與執行緒最大的區別就是不能共享記憶體,因此也不存在鎖競爭問題,兩個Isolate
完全是兩條獨立的執行線,且每個Isolate
都有自己的事件循環,它們之間只能通過發送消息通訊,所以它的資源開銷低於執行緒。
從主Isolate
創建一個新的Isolate
有兩種方法
spawnUri
static Future<Isolate> spawnUri()
spawnUri
方法有三個必須的參數,第一個是Uri,指定一個新Isolate
程式碼文件的路徑,第二個是參數列表,類型是List<String>
,第三個是動態消息。需要注意,用於運行新Isolate
的程式碼文件中,必須包含一個main函數,它是新Isolate
的入口方法,該main函數中的args參數列表,正對應spawnUri
中的第二個參數。如不需要向新Isolate
中傳參數,該參數可傳空List
主Isolate
中的程式碼:
import 'dart:isolate'; void main() { print("main isolate start"); create_isolate(); print("main isolate stop"); } // 創建一個新的 isolate create_isolate() async{ ReceivePort rp = new ReceivePort(); SendPort port1 = rp.sendPort; Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1); SendPort port2; rp.listen((message){ print("main isolate message: $message"); if (message[0] == 0){ port2 = message[1]; }else{ port2?.send([1,"這條資訊是 main isolate 發送的"]); } }); // 可以在適當的時候,調用以下方法殺死創建的 isolate // newIsolate.kill(priority: Isolate.immediate); }
創建other_task.dart
文件,編寫新Isolate
的程式碼
import 'dart:isolate'; import 'dart:io'; void main(args, SendPort port1) { print("isolate_1 start"); print("isolate_1 args: $args"); ReceivePort receivePort = new ReceivePort(); SendPort port2 = receivePort.sendPort; receivePort.listen((message){ print("isolate_1 message: $message"); }); // 將當前 isolate 中創建的SendPort發送到主 isolate中用於通訊 port1.send([0, port2]); // 模擬耗時5秒 sleep(Duration(seconds:5)); port1.send([1, "isolate_1 任務完成"]); print("isolate_1 stop"); }
運行主Isolate
的結果:
main isolate start main isolate stop isolate_1 start isolate_1 args: [hello, isolate, this is args] main isolate message: [0, SendPort] isolate_1 stop main isolate message: [1, isolate_1 任務完成] isolate_1 message: [1, 這條資訊是 main isolate 發送的]

整個消息通訊過程如上圖所示,兩個Isolate是通過兩對Port對象通訊,一對Port分別由用於接收消息的ReceivePort
對象,和用於發送消息的SendPort
對象構成。其中SendPort
對象不用單獨創建,它已經包含在ReceivePort
對象之中。需要注意,一對Port對象只能單向發消息,這就如同一根自來水管,ReceivePort
和SendPort
分別位於水管的兩頭,水流只能從SendPort
這頭流向ReceivePort
這頭。因此,兩個Isolate
之間的消息通訊肯定是需要兩根這樣的水管的,這就需要兩對Port對象。
理解了Isolate
消息通訊的原理,那麼在Dart程式碼中,具體是如何操作的呢?

ReceivePort
對象通過調用listen
方法,傳入一個函數可用來監聽並處理髮送來的消息。SendPort
對象則調用send()
方法來發送消息。send
方法傳入的參數可以是null
,num
, bool
, double
,String
, List
,Map
或者是自定義的類。 在上例中,我們發送的是包含兩個元素的List
對象,第一個元素是整型,表示消息類型,第二個元素則表示消息內容。
spawn
static Future<Isolate> spawn()
除了使用spawnUri
,更常用的是使用spawn
方法來創建新的Isolate
,我們通常希望將新創建的Isolate
程式碼和main Isolate
程式碼寫在同一個文件,且不希望出現兩個main函數,而是將指定的耗時函數運行在新的Isolate
,這樣做有利於程式碼的組織和程式碼的復用。spawn
方法有兩個必須的參數,第一個是需要運行在新Isolate
的耗時函數,第二個是動態消息,該參數通常用於傳送主Isolate
的SendPort
對象。
spawn
的用法與spawnUri
相似,且更為簡潔,將上面例子稍作修改如下
import 'dart:isolate'; import 'dart:io'; void main() { print("main isolate start"); create_isolate(); print("main isolate end"); } // 創建一個新的 isolate create_isolate() async{ ReceivePort rp = new ReceivePort(); SendPort port1 = rp.sendPort; Isolate newIsolate = await Isolate.spawn(doWork, port1); SendPort port2; rp.listen((message){ print("main isolate message: $message"); if (message[0] == 0){ port2 = message[1]; }else{ port2?.send([1,"這條資訊是 main isolate 發送的"]); } }); } // 處理耗時任務 void doWork(SendPort port1){ print("new isolate start"); ReceivePort rp2 = new ReceivePort(); SendPort port2 = rp2.sendPort; rp2.listen((message){ print("doWork message: $message"); }); // 將新isolate中創建的SendPort發送到主isolate中用於通訊 port1.send([0, port2]); // 模擬耗時5秒 sleep(Duration(seconds:5)); port1.send([1, "doWork 任務完成"]); print("new isolate end"); }
運行結果:
main isolate start main isolate end new isolate start main isolate message: [0, SendPort] new isolate end main isolate message: [1, doWork 任務完成] doWork message: [1, 這條資訊是 main isolate 發送的]
無論是上面的spawn
還是spawnUri
,運行後都會創建兩個進程,一個是主Isolate
的進程,一個是新Isolate
的進程,兩個進程都雙向綁定了消息通訊的通道,即使新的Isolate
中的任務完成了,它的進程也不會立刻退出,因此,當使用完自己創建的Isolate
後,最好調用newIsolate.kill(priority: Isolate.immediate);
將Isolate
立即殺死。
Flutter 中創建Isolate
無論如何,在Dart中創建一個Isolate
都顯得有些繁瑣,可惜的是Dart官方並未提供更高級封裝。但是,如果想在Flutter中創建Isolate
,則有更簡便的API,這是由Flutter
官方進一步封裝ReceivePort
而提供的更簡潔API。詳細API文檔[1]
使用compute
函數來創建新的Isolate
並執行耗時任務
import 'package:flutter/foundation.dart'; import 'dart:io'; // 創建一個新的Isolate,在其中運行任務doWork create_new_task() async{ var str = "New Task"; var result = await compute(doWork, str); print(result); } String doWork(String value){ print("new isolate doWork start"); // 模擬耗時5秒 sleep(Duration(seconds:5)); print("new isolate doWork end"); return "complete:$value"; }
compute
函數有兩個必須的參數,第一個是待執行的函數,這個函數必須是一個頂級函數,不能是類的實例方法,可以是類的靜態方法,第二個參數為動態的消息類型,可以是被運行函數的參數。需要注意,使用compute
應導入'package:flutter/foundation.dart'
包。
使用場景
Isolate
雖好,但也有合適的使用場景,不建議濫用Isolate
,應儘可能多的使用Dart中的事件循環機制去處理非同步任務,這樣才能更好的發揮Dart語言的優勢。
那麼應該在什麼時候使用Future,什麼時候使用Isolate呢?一個最簡單的判斷方法是根據某些任務的平均時間來選擇:
- 方法執行在幾毫秒或十幾毫秒左右的,應使用
Future
- 如果一個任務需要幾百毫秒或之上的,則建議創建單獨的
Isolate
除此之外,還有一些可以參考的場景
- JSON 解碼
- 加密
- 影像處理:比如剪裁
- 網路請求:載入資源、圖片
參考資料:
Dart 文檔[2]
Isolate 文檔[3]
參考資料
[1]
詳細API文檔: https://docs.flutter.io/flutter/foundation/compute.html
[2]
Dart 文檔: https://webdev.dartlang.org/articles/performance/event-loop
[3]
Isolate 文檔: https://api.dartlang.org/stable/2.3.0/dart-isolate/Isolate-class.html