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:
| Stufe | Mechanismus | Anwendbare Szenarien |
|---|---|---|
| Event-Loop | Single-Threaded asynchron (Event-Loop) | I/O-Operationen, Timer und Benutzerinteraktion. |
| Isolate | Isolierter Speicher + Nachrichtenübermittlung | CPU-intensive Berechnung |
| Future/Stream | Asynchrone Programmiersyntax | Die 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
// 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
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
// 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
// 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 werden | Kann nicht übergeben werden |
|---|---|
| null、bool、int、double、String | Funktionen, 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
| Funktionen | Dart-Isolate | Traditionelles Multithreading (z. B. Java) |
|---|---|---|
| Speichermodell | Unabhängiger Speicher, keine gemeinsame Nutzung | Gemeinsam genutzter Speicher |
| Kommunikationsmethode | Nachrichtenaustausch (SendPort) | Gemeinsam genutzte Variablen + Sperren |
| Datenrennen | Existiert nicht (keine gemeinsame Nutzung) | Erfordert Schutz durch Sperrmechanismus |
| Deadlock-Risiko | Nahezu nicht vorhanden | Exists |
| Erzeugungsaufwand | Relativ groß | Relativ klein |
| Anwendbare Szenarien | CPU-intensive parallele Berechnung | Allgemeine Nebenläufigkeit |
Weitere ErweiterungenObwohl 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.