內容

Dart 中的並行處理

此頁面包含 Dart 中並行程式設計運作方式的概念概述。它從高層面說明事件迴圈、非同步語言功能和隔離。如需使用 Dart 中並行處理的更多實用程式碼範例,請閱讀 非同步支援 頁面和 隔離 頁面。

Dart 中的並行程式設計是指非同步 API,例如 FutureStream,以及 隔離,它允許您將處理移至個別核心。

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

事件迴圈

#

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

隨著您的應用程式執行,所有事件都會新增到佇列中,稱為 事件佇列。事件可以是任何內容,從重新繪製使用者介面的要求到使用者的點選和按鍵,再到來自磁碟的 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 和非同步等待。這些 API 建構於此事件迴圈之上。

例如,考慮建立網路要求

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

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

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

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

非同步程式設計

#

此區段總結 Dart 中非同步程式設計的不同類型和語法。如果您已經熟悉 FutureStream 和非同步等待,則可以跳到 隔離區段

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

非同步-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

串流

#

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,請瀏覽 非同步程式設計 codelab

隔離

#

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

所有 Dart 程式碼都在隔離中執行,而不是執行緒。使用隔離,您的 Dart 程式碼可以同時執行多個獨立任務。隔離類似於執行緒或處理程序,但每個隔離都有自己的記憶體和執行事件迴圈的單一執行緒。

每個隔離都有自己的全域欄位,確保隔離中的任何狀態都無法從任何其他隔離中存取。隔離只能透過訊息傳遞彼此通訊。隔離之間沒有共用狀態,表示並行運算的複雜性,例如 互斥鎖或鎖定資料競爭,不會在 Dart 中發生。話雖如此,隔離並不會完全防止競爭條件。如需有關此並行運算模型的更多資訊,請閱讀 Actor 模型

使用隔離,您的 Dart 程式碼可以同時執行多個獨立任務,並在有需要時使用額外的處理器核心。隔離類似於執行緒或處理程序,但每個隔離都有自己的記憶體和執行事件迴圈的單一執行緒。

主要分離

#

在多數情況下,您完全不需要考慮 Isolate。Dart 程式預設會在主 Isolate 中執行。這是程式開始執行和運作的執行緒,如下圖所示

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

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

分離生命週期

#

如下圖所示,每個 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。

使用分離

#

依據使用案例,有兩種方式可在 Dart 中使用 Isolate

  • 使用 Isolate.run() 在單獨執行緒上執行單一運算。
  • 使用 Isolate.spawn() 建立一個 Isolate,這個 Isolate 將會隨著時間處理多則訊息,或是一個背景工作執行緒。如需更多關於使用長期 Isolate 的資訊,請閱讀 Isolates 頁面。

在大部分情況下,建議使用 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.spawn() 時,這兩個 Isolate 會有相同的可執行程式碼,並在同一個Isolate 群組中。Isolate 群組能啟用效能最佳化,例如共享程式碼;一個新的 Isolate 會立即執行 Isolate 群組擁有的程式碼。此外,Isolate.exit() 僅在 Isolate 位於同一個 Isolate 群組時才會運作。

在某些特殊情況下,你可能需要使用 Isolate.spawnUri(),它會設定新的 Isolate,並提供位於指定 URI 的程式碼副本。不過,spawnUri()spawn() 慢很多,而且新的 Isolate 也不在它的產生器 Isolate 群組中。另一個效能影響是,當 Isolate 位於不同的群組時,訊息傳遞會較慢。

分離的限制

#

Isolates 不是執行緒

#

如果你從一個具有多執行緒的語言轉換到 Dart,你可能會合理地預期 Isolate 會像執行緒一樣運作,但事實並非如此。每個 Isolate 都擁有自己的狀態,確保 Isolate 中的任何狀態都無法從任何其他 Isolate 存取。因此,Isolates 受到它們對自己記憶體存取的限制。

例如,如果你有一個具有全域可變變數的應用程式,那個變數將會是你的產生 Isolate 中的單獨變數。如果你在產生的 Isolate 中變動那個變數,它在主 Isolate 中將保持不變。這就是 Isolate 的運作方式,當你考慮使用 Isolate 時,務必記住這一點。

訊息類型

#

透過 SendPort 傳送的訊息幾乎可以是任何類型的 Dart 物件,但有少數例外

除了這些例外,任何物件都可以傳送。請查看 SendPort.send 文件以取得更多資訊。

請注意,Isolate.spawn()Isolate.exit() 會抽象化 SendPort 物件,因此它們受到相同的限制。

網頁上的並行處理

#

所有 Dart 應用程式都可以使用 async-awaitFutureStream 進行非封鎖、交錯運算。不過,Dart 網路平台 不支援 Isolate。Dart 網路應用程式可以使用 網頁工作執行緒 在背景執行緒中執行指令碼,類似於 Isolate。不過,網頁工作執行緒的功能和能力與 Isolate 有些不同。

例如,當網頁工作執行緒在執行緒之間傳送資料時,它們會將資料來回複製。不過,資料複製可能會非常慢,特別是對於大型訊息。Isolate 執行相同的動作,但也會提供 API,可以更有效率地傳輸儲存訊息的記憶體。

建立網頁工作執行緒和 Isolate 的方式也不同。您只能透過宣告一個獨立的程式進入點並分別編譯它來建立網頁工作執行緒。啟動網頁工作執行緒類似於使用 Isolate.spawnUri 來啟動 Isolate。您也可以使用 Isolate.spawn 來啟動 Isolate,這需要較少的資源,因為它會重複使用部分與產生 Isolate 相同的程式碼和資料。網頁工作執行緒沒有等效的 API。

其他資源

#