Dart 中的並行處理
本頁包含 Dart 中並行程式設計如何運作的概念性總覽。它從高階角度解釋事件迴圈、async 語言功能和 Isolate。如需在 Dart 中使用並行處理的更多實用程式碼範例,請閱讀非同步支援頁面和Isolate頁面。
Dart 中的並行程式設計指的是非同步 API (例如 Future
和 Stream
) 以及Isolate,後者可讓您將程序移至不同的核心。
所有 Dart 程式碼都在 Isolate 中執行,從預設的主要 Isolate 開始,並可選擇性地擴展到您明確建立的任何後續 Isolate。當您產生新的 Isolate 時,它有自己的隔離記憶體和自己的事件迴圈。事件迴圈是讓 Dart 中的非同步和並行程式設計成為可能的關鍵。
事件迴圈
#Dart 的執行階段模型基於事件迴圈。事件迴圈負責執行您的程式碼、收集和處理事件等等。
當您的應用程式執行時,所有事件都會新增到一個稱為事件佇列的佇列中。事件可以是任何事物,從重新繪製 UI 的請求,到使用者點擊和按鍵,再到來自磁碟的 I/O。由於您的應用程式無法預測事件發生的順序,因此事件迴圈會按照事件排隊的順序,一次處理一個事件。
事件迴圈的功能方式類似於此程式碼
while (eventQueue.waitForEvent()) {
eventQueue.processNextEvent();
}
此範例事件迴圈是同步的,並在單一執行緒上執行。但是,大多數 Dart 應用程式需要一次執行多項操作。例如,用戶端應用程式可能需要執行 HTTP 請求,同時也監聽使用者點擊按鈕。為了處理這個問題,Dart 提供了許多非同步 API,例如Future、Stream 和 async-await。這些 API 都是圍繞此事件迴圈建構的。
例如,考慮發出網路請求
http.get('https://example.com').then((response) {
if (response.statusCode == 200) {
print('Success!');
}
}
當此程式碼到達事件迴圈時,它會立即呼叫第一個子句 http.get
,並傳回 Future
。它也會告知事件迴圈保留 then()
子句中的回呼,直到 HTTP 請求解析為止。當發生這種情況時,它應該執行該回呼,並將請求的結果作為引數傳遞。
此相同模型通常是事件迴圈如何處理 Dart 中的所有其他非同步事件,例如 Stream
物件。
非同步程式設計
#本節總結了 Dart 中非同步程式設計的不同類型和語法。如果您已經熟悉 Future
、Stream
和 async-await,則可以跳到Isolate 章節。
Future
#Future
代表非同步操作的結果,該操作最終將完成並傳回值或錯誤。
在此範例程式碼中,Future<String>
的傳回類型代表最終提供 String
值 (或錯誤) 的承諾。
Future<String> _readFileAsync(String filename) {
final file = File(filename);
// .readAsString() returns a Future.
// .then() registers a callback to be executed when `readAsString` resolves.
return file.readAsString().then((contents) {
return contents.trim();
});
}
async-await 語法
#async
和 await
關鍵字提供了一種宣告式方式來定義非同步函式並使用其結果。
以下是一些同步程式碼的範例,這些程式碼在等待檔案 I/O 時會被封鎖
const String filename = 'with_keys.json';
void main() {
// Read some data.
final fileData = _readFileSync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
String _readFileSync() {
final file = File(filename);
final contents = file.readAsStringSync();
return contents.trim();
}
以下是類似的程式碼,但進行了變更 (已醒目提示) 以使其成為非同步
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
main()
函式在 _readFileAsync()
前面使用 await
關鍵字,讓其他 Dart 程式碼 (例如事件處理常式) 在原生程式碼 (檔案 I/O) 執行時使用 CPU。使用 await
也具有將 _readFileAsync()
傳回的 Future<String>
轉換為 String
的效果。因此,contents
變數具有隱含類型 String
。
如下圖所示,Dart 程式碼在 readAsString()
執行非 Dart 程式碼 (在 Dart 執行階段或作業系統中) 時暫停。一旦 readAsString()
傳回值,Dart 程式碼執行就會恢復。
Stream
#Dart 也支援串流形式的非同步程式碼。串流在未來和一段時間內重複提供值。在一段時間內提供一系列 int
值的承諾具有 Stream<int>
類型。
在以下範例中,使用 Stream.periodic
建立的串流每秒重複發出新的 int
值。
Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i);
await-for 和 yield
#Await-for 是一種 for 迴圈類型,它會在提供新值時執行迴圈的每個後續迭代。換句話說,它用於「迴圈處理」串流。在此範例中,當從作為引數提供的串流發出新值時,將從函式 sumStream
發出新值。yield
關鍵字用於傳回值串流的函式中,而不是 return
。
Stream<int> sumStream(Stream<int> stream) async* {
var sum = 0;
await for (final value in stream) {
yield sum += value;
}
}
如果您想了解更多關於使用 async
、await
、Stream
和 Future
的資訊,請查看非同步程式設計教學課程。
Isolate
#除了非同步 API 之外,Dart 也透過 Isolate 支援並行處理。大多數現代裝置都具有多核心 CPU。為了利用多個核心,開發人員有時會使用並行執行的共用記憶體執行緒。但是,共用狀態並行處理容易出錯,並且可能導致程式碼複雜化。
所有 Dart 程式碼都在 Isolate 內執行,而不是執行緒。使用 Isolate,您的 Dart 程式碼可以同時執行多個獨立的任務,如果有多個處理器核心可用,則可以使用額外的處理器核心。Isolate 類似於執行緒或程序,但每個 Isolate 都有自己的記憶體和執行事件迴圈的單一執行緒。
每個 Isolate 都有自己的全域欄位,確保 Isolate 中的任何狀態都無法從任何其他 Isolate 存取。Isolate 只能透過訊息傳遞相互通訊。Isolate 之間沒有共用狀態表示並行處理複雜性 (例如互斥鎖或鎖定) 和資料競爭) 不會在 Dart 中發生。也就是說,Isolate 並不能完全避免競爭條件。如需有關此並行處理模型的更多資訊,請閱讀關於Actor 模型的資訊。
主要 Isolate
#在大多數情況下,您根本不需要考慮 Isolate。Dart 程式預設在主要 Isolate 中執行。它是程式開始執行和執行的執行緒,如下圖所示
即使是單一 Isolate 程式也能順暢執行。在繼續下一行程式碼之前,這些應用程式會使用async-await 來等待非同步操作完成。行為良好的應用程式會快速啟動,並儘快進入事件迴圈。然後,應用程式會及時回應每個排隊的事件,並在必要時使用非同步操作。
Isolate 生命週期
#如下圖所示,每個 Isolate 都從執行一些 Dart 程式碼開始,例如 main()
函式。此 Dart 程式碼可能會註冊一些事件監聽器,以回應使用者輸入或檔案 I/O 等。當 Isolate 的初始函式傳回時,如果需要處理事件,Isolate 會繼續存在。處理完事件後,Isolate 會退出。
事件處理
#在用戶端應用程式中,主要 Isolate 的事件佇列可能包含重新繪製請求以及點擊和其他 UI 事件的通知。例如,下圖顯示一個重新繪製事件,然後是一個點擊事件,然後是兩個重新繪製事件。事件迴圈以先進先出的順序從佇列中取得事件。
事件處理發生在 main()
退出後的主要 Isolate 上。在下圖中,在 main()
退出後,主要 Isolate 會處理第一個重新繪製事件。之後,主要 Isolate 會處理點擊事件,然後是重新繪製事件。
如果同步操作花費過多的處理時間,應用程式可能會變得沒有回應。在下圖中,點擊處理程式碼花費的時間太長,因此後續事件的處理時間太晚。應用程式可能會顯示凍結,並且它執行的任何動畫都可能不流暢。
在用戶端應用程式中,耗時過長的同步操作的結果通常是卡頓 (不流暢) 的 UI 動畫。更糟的是,UI 可能會完全沒有回應。
背景工作程序
#如果您的應用程式的 UI 因為耗時的計算而變得沒有回應 (例如剖析大型 JSON 檔案),請考慮將該計算卸載到工作程序 Isolate (通常稱為背景工作程序)。如下圖所示,常見的情況是產生一個簡單的工作程序 Isolate,它執行計算,然後退出。工作程序 Isolate 會在退出時在訊息中傳回其結果。
工作程序 Isolate 可以執行 I/O (例如讀取和寫入檔案)、設定計時器等等。它有自己的記憶體,並且不與主要 Isolate 共用任何狀態。工作程序 Isolate 可以封鎖,而不會影響其他 Isolate。
使用 Isolate
#在 Dart 中有兩種使用 Isolate 的方式,具體取決於使用案例
- 使用
Isolate.run()
在個別執行緒上執行單一計算。 - 使用
Isolate.spawn()
建立一個 Isolate,它將在一段時間內處理多個訊息,或建立一個背景工作程序。如需有關使用長期 Isolate 的更多資訊,請閱讀Isolate頁面。
在大多數情況下,Isolate.run
是建議在背景執行程序的 API。
Isolate.run()
#靜態 Isolate.run()
方法需要一個引數:將在新產生的 Isolate 上執行的回呼。
int slowFib(int n) => n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
// Compute without blocking current isolate.
void fib40() async {
var result = await Isolate.run(() => slowFib(40));
print('Fib(40) = $result');
}
效能與 Isolate 群組
#當 Isolate 呼叫 Isolate.spawn()
時,這兩個 Isolate 具有相同的可執行程式碼,並且位於相同的Isolate 群組中。Isolate 群組可實現效能最佳化,例如共用程式碼;新的 Isolate 會立即執行 Isolate 群組擁有的程式碼。此外,Isolate.exit()
僅在 Isolate 位於相同的 Isolate 群組中時才有效。
在某些特殊情況下,您可能需要使用 Isolate.spawnUri()
,它會使用指定 URI 處的程式碼副本設定新的 Isolate。但是,spawnUri()
比 spawn()
慢得多,而且新的 Isolate 不在其產生器的 Isolate 群組中。另一個效能後果是,當 Isolate 位於不同的群組中時,訊息傳遞速度會較慢。
Isolate 的限制
#Isolate 不是執行緒
#如果您是從具有多執行緒的語言轉到 Dart,則很可能會期望 Isolate 的行為類似於執行緒,但事實並非如此。每個 Isolate 都有自己的狀態,確保 Isolate 中的任何狀態都無法從任何其他 Isolate 存取。因此,Isolate 受限於其對自身記憶體的存取。
例如,如果您有一個具有全域可變變數的應用程式,則該變數將是您產生的 Isolate 中的個別變數。如果您在產生的 Isolate 中變更該變數,它在主要 Isolate 中將保持不變。這就是 Isolate 的預期功能,並且在您考慮使用 Isolate 時,務必牢記這一點。
訊息類型
#透過 SendPort
傳送的訊息可以是幾乎任何類型的 Dart 物件,但也有一些例外情況
- 具有原生資源的物件,例如
Socket
。 ReceivePort
DynamicLibrary
Finalizable
Finalizer
NativeFinalizer
Pointer
UserTag
- 標記為
@pragma('vm:isolate-unsendable')
的類別的實例
除了這些例外情況之外,任何物件都可以傳送。查看 SendPort.send
文件以取得更多資訊。
請注意,Isolate.spawn()
和 Isolate.exit()
會抽象化 SendPort
物件,因此它們也受到相同的限制。
Isolate 之間的同步封鎖通訊
#可以並行執行的 Isolate 數量有限制。此限制不會影響 Dart 中 Isolate 之間透過訊息進行的標準非同步通訊。您可以同時執行數百個 Isolate 並取得進展。Isolate 會以循環配置資源的方式在 CPU 上排程,並經常相互讓出。
Isolate 只能在純 Dart 之外同步通訊,方法是透過 FFI 使用 C 程式碼來執行此操作。如果 Isolate 的數量超過限制,則嘗試透過 FFI 呼叫中的同步封鎖在 Isolate 之間進行同步通訊可能會導致死結,除非採取特殊措施。此限制並非硬式編碼為特定數字,而是根據 Dart 應用程式可用的 Dart VM 堆積大小計算得出的。
為了避免這種情況,執行同步封鎖的 C 程式碼需要在執行封鎖操作之前離開目前的 Isolate,並在從 FFI 呼叫傳回 Dart 之前重新進入它。閱讀關於 Dart_EnterIsolate
和 Dart_ExitIsolate
以了解更多資訊。
Web 上的並行處理
#所有 Dart 應用程式都可以使用 async-await
、Future
和 Stream
進行非封鎖、交錯的計算。但是,Dart Web 平台 不支援 Isolate。Dart Web 應用程式可以使用 Web Worker 在類似於 Isolate 的背景執行緒中執行指令碼。但是,Web Worker 的功能和功能與 Isolate 有些不同。
例如,當 Web Worker 在執行緒之間傳送資料時,它們會來回複製資料。資料複製可能非常慢,尤其是對於大型訊息。Isolate 也執行相同的操作,但也提供可以更有效率地傳輸保存訊息的記憶體的 API。
建立 Web Worker 和 Isolate 也不同。您只能透過宣告個別的程式進入點並單獨編譯它來建立 Web Worker。啟動 Web Worker 類似於使用 Isolate.spawnUri
啟動 Isolate。您也可以使用 Isolate.spawn
啟動 Isolate,這需要的資源較少,因為它會重複使用與產生 Isolate 相同的某些程式碼和資料。Web Worker 沒有對等的 API。
其他資源
#- 如果您使用許多 Isolate,請考慮 Flutter 中的
IsolateNameServer
,或package:isolate_name_server
,它為非 Flutter Dart 應用程式提供類似的功能。 - 閱讀更多關於 Actor 模型的資訊,Dart 的 Isolate 就是基於該模型。
- 關於
Isolate
API 的其他文件
除非另有說明,否則本網站上的文件反映的是 Dart 3.7.1。頁面上次更新時間為 2024-11-17。 查看原始碼 或 回報問題。