跳至主要內容

Dart 中的並行處理

本頁包含 Dart 中並行程式設計如何運作的概念性總覽。它從高階角度解釋事件迴圈、async 語言功能和 Isolate。如需在 Dart 中使用並行處理的更多實用程式碼範例,請閱讀非同步支援頁面和Isolate頁面。

Dart 中的並行程式設計指的是非同步 API (例如 FutureStream) 以及Isolate,後者可讓您將程序移至不同的核心。

所有 Dart 程式碼都在 Isolate 中執行,從預設的主要 Isolate 開始,並可選擇性地擴展到您明確建立的任何後續 Isolate。當您產生新的 Isolate 時,它有自己的隔離記憶體和自己的事件迴圈。事件迴圈是讓 Dart 中的非同步和並行程式設計成為可能的關鍵。

事件迴圈

#

Dart 的執行階段模型基於事件迴圈。事件迴圈負責執行您的程式碼、收集和處理事件等等。

當您的應用程式執行時,所有事件都會新增到一個稱為事件佇列的佇列中。事件可以是任何事物,從重新繪製 UI 的請求,到使用者點擊和按鍵,再到來自磁碟的 I/O。由於您的應用程式無法預測事件發生的順序,因此事件迴圈會按照事件排隊的順序,一次處理一個事件。

A figure showing events being fed, one by one, into the
event loop

事件迴圈的功能方式類似於此程式碼

dart
while (eventQueue.waitForEvent()) {
  eventQueue.processNextEvent();
}

此範例事件迴圈是同步的,並在單一執行緒上執行。但是,大多數 Dart 應用程式需要一次執行多項操作。例如,用戶端應用程式可能需要執行 HTTP 請求,同時也監聽使用者點擊按鈕。為了處理這個問題,Dart 提供了許多非同步 API,例如Future、Stream 和 async-await。這些 API 都是圍繞此事件迴圈建構的。

例如,考慮發出網路請求

dart
http.get('https://example.com').then((response) {
  if (response.statusCode == 200) {
    print('Success!');
  }  
}

當此程式碼到達事件迴圈時,它會立即呼叫第一個子句 http.get,並傳回 Future。它也會告知事件迴圈保留 then() 子句中的回呼,直到 HTTP 請求解析為止。當發生這種情況時,它應該執行該回呼,並將請求的結果作為引數傳遞。

Figure showing async events being added to an event loop and
holding onto a callback to execute later
.

此相同模型通常是事件迴圈如何處理 Dart 中的所有其他非同步事件,例如 Stream 物件。

非同步程式設計

#

本節總結了 Dart 中非同步程式設計的不同類型和語法。如果您已經熟悉 FutureStream 和 async-await,則可以跳到Isolate 章節

Future

#

Future 代表非同步操作的結果,該操作最終將完成並傳回值或錯誤。

在此範例程式碼中,Future<String> 的傳回類型代表最終提供 String 值 (或錯誤) 的承諾。

dart
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 語法

#

asyncawait 關鍵字提供了一種宣告式方式來定義非同步函式並使用其結果。

以下是一些同步程式碼的範例,這些程式碼在等待檔案 I/O 時會被封鎖

dart
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();
}

以下是類似的程式碼,但進行了變更 (已醒目提示) 以使其成為非同步

dart
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 程式碼執行就會恢復。

Flowchart-like figure showing app code executing from start to exit, waiting
for native I/O in between

Stream

#

Dart 也支援串流形式的非同步程式碼。串流在未來和一段時間內重複提供值。在一段時間內提供一系列 int 值的承諾具有 Stream<int> 類型。

在以下範例中,使用 Stream.periodic 建立的串流每秒重複發出新的 int 值。

dart
Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i);

await-for 和 yield

#

Await-for 是一種 for 迴圈類型,它會在提供新值時執行迴圈的每個後續迭代。換句話說,它用於「迴圈處理」串流。在此範例中,當從作為引數提供的串流發出新值時,將從函式 sumStream 發出新值。yield 關鍵字用於傳回值串流的函式中,而不是 return

dart
Stream<int> sumStream(Stream<int> stream) async* {
  var sum = 0;
  await for (final value in stream) {
    yield sum += value;
  }
}

如果您想了解更多關於使用 asyncawaitStreamFuture 的資訊,請查看非同步程式設計教學課程

Isolate

#

除了非同步 API 之外,Dart 也透過 Isolate 支援並行處理。大多數現代裝置都具有多核心 CPU。為了利用多個核心,開發人員有時會使用並行執行的共用記憶體執行緒。但是,共用狀態並行處理容易出錯,並且可能導致程式碼複雜化。

所有 Dart 程式碼都在 Isolate 內執行,而不是執行緒。使用 Isolate,您的 Dart 程式碼可以同時執行多個獨立的任務,如果有多個處理器核心可用,則可以使用額外的處理器核心。Isolate 類似於執行緒或程序,但每個 Isolate 都有自己的記憶體和執行事件迴圈的單一執行緒。

每個 Isolate 都有自己的全域欄位,確保 Isolate 中的任何狀態都無法從任何其他 Isolate 存取。Isolate 只能透過訊息傳遞相互通訊。Isolate 之間沒有共用狀態表示並行處理複雜性 (例如互斥鎖或鎖定) 和資料競爭) 不會在 Dart 中發生。也就是說,Isolate 並不能完全避免競爭條件。如需有關此並行處理模型的更多資訊,請閱讀關於Actor 模型的資訊。

主要 Isolate

#

在大多數情況下,您根本不需要考慮 Isolate。Dart 程式預設在主要 Isolate 中執行。它是程式開始執行和執行的執行緒,如下圖所示

A figure showing a main isolate, which runs , responds to events,
and then exits

即使是單一 Isolate 程式也能順暢執行。在繼續下一行程式碼之前,這些應用程式會使用async-await 來等待非同步操作完成。行為良好的應用程式會快速啟動,並儘快進入事件迴圈。然後,應用程式會及時回應每個排隊的事件,並在必要時使用非同步操作。

Isolate 生命週期

#

如下圖所示,每個 Isolate 都從執行一些 Dart 程式碼開始,例如 main() 函式。此 Dart 程式碼可能會註冊一些事件監聽器,以回應使用者輸入或檔案 I/O 等。當 Isolate 的初始函式傳回時,如果需要處理事件,Isolate 會繼續存在。處理完事件後,Isolate 會退出。

A more general figure showing that any isolate runs some code, optionally responds to events, and then exits

事件處理

#

在用戶端應用程式中,主要 Isolate 的事件佇列可能包含重新繪製請求以及點擊和其他 UI 事件的通知。例如,下圖顯示一個重新繪製事件,然後是一個點擊事件,然後是兩個重新繪製事件。事件迴圈以先進先出的順序從佇列中取得事件。

A figure showing events being fed, one by one, into the event loop

事件處理發生在 main() 退出後的主要 Isolate 上。在下圖中,在 main() 退出後,主要 Isolate 會處理第一個重新繪製事件。之後,主要 Isolate 會處理點擊事件,然後是重新繪製事件。

如果同步操作花費過多的處理時間,應用程式可能會變得沒有回應。在下圖中,點擊處理程式碼花費的時間太長,因此後續事件的處理時間太晚。應用程式可能會顯示凍結,並且它執行的任何動畫都可能不流暢。

A figure showing a tap handler with a too-long execution time

在用戶端應用程式中,耗時過長的同步操作的結果通常是卡頓 (不流暢) 的 UI 動畫。更糟的是,UI 可能會完全沒有回應。

背景工作程序

#

如果您的應用程式的 UI 因為耗時的計算而變得沒有回應 (例如剖析大型 JSON 檔案),請考慮將該計算卸載到工作程序 Isolate (通常稱為背景工作程序)。如下圖所示,常見的情況是產生一個簡單的工作程序 Isolate,它執行計算,然後退出。工作程序 Isolate 會在退出時在訊息中傳回其結果。

A figure showing a main isolate and a simple worker 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 上執行的回呼。

dart
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 物件,但也有一些例外情況

除了這些例外情況之外,任何物件都可以傳送。查看 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_EnterIsolateDart_ExitIsolate 以了解更多資訊。

Web 上的並行處理

#

所有 Dart 應用程式都可以使用 async-awaitFutureStream 進行非封鎖、交錯的計算。但是,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。

其他資源

#