Dart-Nebenläufigkeit und Isolate

Nebenläufigkeit ist die Fähigkeit, mehrere Aufgaben gleichzeitig zu bearbeiten.

Darts Nebenläufigkeitsmodell unterscheidet sich vom traditionellen Multithreading; es verwendet Isolates, um parallele Berechnung zu erreichen.

Dieses Kapitel stellt Darts Nebenläufigkeitsmodell, das Konzept und die Verwendung von Isolates sowie den Nachrichtenübermittlungsmechanismus vor.


Dart-Nebenläufigkeitsmodell

Die meisten Programmiersprachen verwenden ein Multithreading-Modell mit gemeinsamem Speicher, bei dem mehrere Threads denselben Speicherbereich teilen.

Der Nachteil dieses Modells ist, dass es anfällig für Race Conditions und Deadlocks ist.

Dart verfolgt eine andere Strategie:Jedes Isolate hat seinen eigenen unabhängigen Speicher-HeapIsolates teilen keinen Speicher.

Sie kommunizieren über Nachrichtenübermittlung, was Data-Race-Probleme grundlegend vermeidet.

Darts Nebenläufigkeitsmodell hat drei Ebenen:

StufeMechanismusAnwendbare Szenarien
Event-LoopSingle-Threaded asynchron (Event-Loop)I/O-Operationen, Timer und Benutzerinteraktion.
IsolateIsolierter Speicher + NachrichtenübermittlungCPU-intensive Berechnung
Future/StreamAsynchrone ProgrammiersyntaxDie meisten alltäglichen Entwicklungsszenarien

Die Nebenläufigkeitsanforderungen der meisten Dart-Programme lassen sich mit async/await und Future/Stream erfüllen. Isolates sind nur notwendig, wenn Sie schwere CPU-gebundene Berechnungen durchführen müssen. Isolates vorzeitig einzuführen macht den Code komplexer bei geringem Nutzen.


Isolate-Konzept und -Verwendung

Ein Isolate ist Darts Einheit der Nebenläufigkeit, und jedes Isolate hat seinen eigenen unabhängigen Speicher und Event-Loop.

Das Hauptprogramm selbst läuft in einem Isolate (dem Haupt-Isolate).

Verwende Isolate.spawn, um ein neues Isolate zu erstellen

Beispiel

import 'dart:isolate';

// Diese Funktion wird im neuen Isolate ausgeführt
// SendPort wird verwendet, um Nachrichten an das Haupt-Isolate zu senden
void heavyComputation(SendPort sendPort) {
  print('Neues Isolate beginnt zu rechnen...');

  // Simuliere CPU-intensive Berechnung
  int sum = 0;
  for (int i = 1; i <= 10000000; i++) {
    sum += i;
  }

  // Das Berechnungsergebnis zurück an das Haupt-Isolate senden
  sendPort.send(sum);

  print('Berechnung des neuen Isolates abgeschlossen, Ergebnis gesendet');
}

Future<void> main() async {
  print('Haupt-Isolate gestartet');

  // ReceivePort zum Empfangen von Nachrichten erstellen
  var receivePort = ReceivePort();

  // spawn erstellt ein neues Isolate
  await Isolate.spawn(heavyComputation, receivePort.sendPort);

  print('Das Haupt-Isolate kann andere Dinge tun, während es auf das Ergebnis wartet...');

  // Auf das Berechnungsergebnis vom neuen Isolate warten
  var result = await receivePort.first;
  print('BEISPIEL Berechnungsergebnis: Summe von 1 bis 10000000 = $result');

  receivePort.close();
  print('Haupt-Isolate beendet');
}
主 Isolate 启动
主 Isolate 在等待结果的同时可以做其他事情...
新 Isolate 开始计算...
新 Isolate 计算完成,结果已发送
EXAMPLE 计算结果: 1 到 10000000 的和 = 50000005000000
主 Isolate 结束

Vergleich: Leistungsunterschiede mit und ohne Isolate.

Beispiel

// Führe eine CPU-intensive Aufgabe im Haupt-Isolate aus (wird blockieren)
int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

void main() {
  // Berechne im Haupt-Isolate — blockiert alle anderen Operationen
  var startTime = DateTime.now();

  // Hinweis: Wenn n zu groß ist, wird diese Berechnung sehr zeitaufwendig.
  // In der realen Entwicklung sollte diese Art von Berechnung in ein separates Isolate ausgelagert werden.
  var result = fibonacci(40);

  var elapsed = DateTime.now().difference(startTime);
  print('BEISPIEL Fibonacci(40) = $result');
  print('Verstrichene Zeit: ${elapsed.inMilliseconds}ms');
  print('(Hinweis: Während der Berechnung kann das Haupt-Isolate keine anderen Aufgaben verarbeiten)');
}

In Flutter-Anwendungen: Wenn eine zeitaufwändige synchrone Berechnung im Haupt-Isolate ausgeführt wird, führt dies zum Einfrieren der UI. Die Lösung besteht darin, die Berechnungsaufgabe in ein separates Isolate auszulagern und nach Abschluss der Berechnung das Ergebnis über Nachrichten zurückzusenden und die UI zu aktualisieren.


Mechanismus zur Nachrichtenübermittlung

Isolates kommunizieren, indem sie Nachrichten über SendPort und ReceivePort austauschen.

Nachrichten müssen serialisierbar sein (primitive Typen, String, List, Map usw.); Funktionen oder Closures können nicht übergeben werden.

Bidirektionale Kommunikation

Beispiel

import 'dart:isolate';

// Die Worker-Funktion, die im neuen Isolate ausgeführt wird
void workerIsolate(SendPort mainSendPort) {
  // Erstellt ein eigenes ReceivePort, um Nachrichten vom Haupt-Isolate zu empfangen
  var workerReceivePort = ReceivePort();

  // Zuerst den SendPort des Workers an das Haupt-Isolate senden, um einen bidirektionalen Kanal aufzubauen
  mainSendPort.send(workerReceivePort.sendPort);

  print('Worker Isolate: Warte auf Aufgaben...');

  // Auf vom Haupt-Isolate gesendete Aufgaben warten
  workerReceivePort.listen((message) {
    if (message is List<int>) {
      // Empfangene Aufgabe: Jede Zahl in der Liste quadrieren
      print('Worker Isolate: empfangene Daten $message');
      var result = message.map((n) => n * n).toList();

      // Das Ergebnis über mainSendPort zurücksenden
      mainSendPort.send(result);
    } else if (message == 'exit') {
      print('Worker Isolate: Exit-Signal empfangen, wird heruntergefahren');
      workerReceivePort.close();
      mainSendPort.send('Tschüss');
    }
  });
}

Future<void> main() async {
  print('Haupt-Isolate: startet');

  // Haupt-Empfangsport erstellen
  var mainReceivePort = ReceivePort();

  // Worker-Isolate starten
  await Isolate.spawn(workerIsolate, mainReceivePort.sendPort);

  // Warten, bis der Worker seinen SendPort sendet (Zwei-Wege-Kommunikation herstellen)
  SendPort? workerSendPort;
  await for (var msg in mainReceivePort) {
    if (msg is SendPort) {
      workerSendPort = msg;
      print('Haupt-Isolate: Zwei-Wege-Kommunikation mit Worker aufgebaut');
      break;
    }
  }

  // Aufgabe über den SendPort des Workers senden
  workerSendPort!.send([1, 2, 3, 4, 5]);
  workerSendPort.send([10, 20, 30]);

  // Berechnungsergebnis des Workers empfangen
  int responseCount = 0;
  await for (var msg in mainReceivePort) {
    if (msg is List<int>) {
      print('Haupt-Isolate: Ergebnis $msg empfangen');
      responseCount++;
      if (responseCount == 2) break;
    }
  }

  // Exit-Signal senden
  workerSendPort.send('exit');
  await for (var msg in mainReceivePort) {
    if (msg == 'goodbye') {
      print('Haupt-Isolate: Worker wurde beendet');
      break;
    }
  }

  mainReceivePort.close();
  print('BEISPIEL Zwei-Wege-Kommunikationsdemo beendet');
}
主 Isolate: 启动
主 Isolate: 已建立与 Worker 的双向通信
Worker Isolate: 等待任务...
Worker Isolate: 收到数据 [1, 2, 3, 4, 5]
主 Isolate: 收到结果 [1, 4, 9, 16, 25]
Worker Isolate: 收到数据 [10, 20, 30]
主 Isolate: 收到结果 [100, 400, 900]
Worker Isolate: 收到退出信号,关闭
主 Isolate: Worker 已退出
EXAMPLE 双向通信演示结束

Vereinfachen mit Isolate.run (Dart 3.0+)

Dart 3.0 führte Isolate.run() ein und vereinfachte die Verwendung einmaliger Berechnungsaufgaben erheblich.

Beispiel

import 'dart:isolate';

// Eine zeitaufwändige Berechnungsfunktion
int complexCalculation(int n) {
  int result = 0;
  for (int i = 0; i < n; i++) {
    result += i * i;
  }
  return result;
}

Future<void> main() async {
  print('Berechnung wird gestartet...');

  // Isolate.run: Eine Zeile Code, erstellt automatisch ein Isolate, führt aus und gibt das Ergebnis zurück
  var result = await Isolate.run(() => complexCalculation(10000000));
  print('BEISPIEL Berechnungsergebnis: $result');

  print('Berechnung abgeschlossen');

  // Vergleich: wenn Isolate nicht verwendet wird
  print('(Wenn direkt im Haupt-Isolate berechnet wird, blockiert es andere Vorgänge)');
}
开始计算...
EXAMPLE 计算结果: 333333283333335000000
计算完成
(如果在主 Isolate 中直接计算,会阻塞其他操作)

Isolate.run() ist geeignet für Szenarien, in denen Sie "eine Berechnung einmal ausführen und das Ergebnis zurückgeben" möchten. Wenn Sie kontinuierliche, bidirektionale Kommunikation benötigen (z. B. einen lang laufenden Hintergrunddienst), müssen Sie Isolate.spawn() + SendPort/ReceivePort verwenden.

Einschränkungen der Isolate-Nachrichtenübermittlung.

Kann übergeben werdenKann nicht übergeben werden
null、bool、int、double、StringFunktionen, Closures
List, Map, Set (Elemente können ebenfalls übergeben werden)Stream、Future
SendPort (wird verwendet, um eine Kommunikationskette aufzubauen).Die meisten Objekte von nicht-primitiven Typen
Capability (Berechtigungstoken)Dateihandles, Netzwerk-Sockets
TransferableTypedData (überträgt effizient große Datenblöcke)Benutzerdefinierte Klassen (sofern nicht serialisierbar)

Vergleich der Nebenläufigkeitsmodelle

FunktionenDart-IsolateTraditionelles Multithreading (z. B. Java)
SpeichermodellUnabhängiger Speicher, keine gemeinsame NutzungGemeinsam genutzter Speicher
KommunikationsmethodeNachrichtenaustausch (SendPort)Gemeinsam genutzte Variablen + Sperren
DatenrennenExistiert nicht (keine gemeinsame Nutzung)Erfordert Schutz durch Sperrmechanismus
Deadlock-RisikoNahezu nicht vorhandenExists
ErzeugungsaufwandRelativ großRelativ klein
Anwendbare SzenarienCPU-intensive parallele BerechnungAllgemeine Nebenläufigkeit

Obwohl Darts Isolate-Modell Datenrennen vermeidet, ist der Kompromiss der relativ hohe Overhead bei der Erstellung und Kommunikation. Erstelle nicht Tausende von Isolates – normalerweise reichen ein paar Isolates aus. Für I/O-gebundene Aufgaben ist die Verwendung von async/await die beste Wahl.

Weitere Erweiterungen