În dezvoltarea aplicațiilor client cu Flutter sau a utilitarelor de linie de comandă (CLI) de mare randament scrise în Dart, păstrarea receptivității interfeței utilizator este critică. La 60 de cadre pe secundă (FPS), o aplicație dispune de doar 16,6 milisecunde pentru a procesa logica și a randa cadrul; la ecrane ProMotion de 120 FPS, această fereastră scade la 8,3 milisecunde.
Deoarece Dart execută în mod implicit întregul cod pe un singur fir (Event Loop), o operațiune CPU-intensivă – cum ar fi parsarea unui răspuns JSON de zeci de megabytes, filtrarea a 100.000 de înregistrări dintr-o bază de date SQLite sau aplicarea unor filtre grafice – va bloca instantaneu Event Loop-ul, producând căderi vizibile de cadre (UI jank).
# Firul Principal (UI / Main Event Loop)
# │
# ▼ [Adăugare Sarcini în Coadă]
# ┌─────────────────────────────────────────────────────────────┐
# │ IsolatePool Dispatcher │
# └──────┬──────────────────────┬──────────────────────┬────────┘
# │ (Canal SendPort) │ (Canal SendPort) │ (Canal SendPort)
# ▼ ▼ ▼
# ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
# │ Worker #1 │ │ Worker #2 │ │ Worker #3 │
# │ Heap Izolat │ │ Heap Izolat │ │ Heap Izolat │
# │ Core CPU #0 │ │ Core CPU #1 │ │ Core CPU #2 │
# └──────────────┘ └──────────────┘ └──────────────┘
# │ │ │
# └──────────────────────┼──────────────────────┘
# ▼ [Returnare Rezultate]
# Firul Principal recepționează datele fără jank!
Comparație Tehnică: Mecanisme de Paralelism în Dart
| Abordare Concurentă | Overhead de Pornire | Reutilizare Thread | Consum de Memorie | Scenariu Optim |
|---|---|---|---|---|
Future.wait() |
Zero (Rulează pe același fir) | N/A (același Event Loop) | Minim | Operațiuni I/O concurente (rețea, citiri paralele disc). |
Isolate.run() |
2 – 8 ms per apel | Nu (Isolate-ul este creat și distrus la fiecare apel) | Mediu (alocă și dealocă heap separat de fiecare dată) | Sarcini CPU ocazionale de durată medie (ex: parsare fișier unic). |
IsolatePool Persistent |
Plătit o singură dată la inițializare | Da (Workers persistenți reutilizați indefinit) | Controlat (fixat la numărul de nuclee CPU fizice) | Procesare intensivă continuă, stream-uri video/audio, exporturi mari de date. |
Transferul de Memorie de Mare Viteză cu TransferableTypedData
La transmiterea mesajelor normale între Isolate-uri, Dart copiază conținutul obiectelor dintr-un heap în altul. Pentru tablouri mari de date binare (imagini, fișiere), această copiere generează presiune inutilă pe memorie. Soluția optimă este TransferableTypedData, care transferă direct adresa fizică de memorie (*zero-copy buffer transfer*), invalidând buffer-ul pe isolate-ul sursă în sub 1 microsecundă!
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
typedef Task = (R Function(T), T);
class WorkerTask {
final int taskId;
final R Function(T) computation;
final T data;
final Completer completer;
WorkerTask(this.taskId, this.computation, this.data) : completer = Completer();
}
class IsolatePool {
final int workerCount;
final List _workerSendPorts = [];
final List _workerReceivePorts = [];
final List _isolates = [];
final Map> _pendingTasks = {};
int _nextTaskId = 0;
int _roundRobinIndex = 0;
bool _isStarted = false;
IsolatePool({int? workerCount})
: workerCount = workerCount ?? Platform.numberOfProcessors;
Future start() async {
if (_isStarted) return;
for (int i = 0; i < workerCount; i++) {
final initReceive = ReceivePort();
final isolate = await Isolate.spawn(_workerLoop, initReceive.sendPort);
_isolates.add(isolate);
final workerSendPort = await initReceive.first as SendPort;
_workerSendPorts.add(workerSendPort);
// Port pentru recepționarea rezultatelor de la worker:
final resultReceive = ReceivePort();
_workerReceivePorts.add(resultReceive);
workerSendPort.send(resultReceive.sendPort);
resultReceive.listen((message) {
if (message is (int taskId, dynamic result, String? error)) {
final completer = _pendingTasks.remove(taskId);
if (completer != null) {
if (error != null) {
completer.completeError(Exception(error));
} else {
completer.complete(result);
}
}
}
});
}
_isStarted = true;
print('[IsolatePool] Inițializat cu succes cu $workerCount workeri activi.');
}
Future execute(R Function(T) computation, T data) {
if (!_isStarted) throw StateError('IsolatePool-ul nu este pornit!');
final taskId = _nextTaskId++;
final completer = Completer();
_pendingTasks[taskId] = completer;
// Distribuție uniformă a sarcinilor prin Round-Robin:
final targetPort = _workerSendPorts[_roundRobinIndex];
_roundRobinIndex = (_roundRobinIndex + 1) % workerCount;
targetPort.send((taskId, computation, data));
return completer.future;
}
static void _workerLoop(SendPort mainSendPort) async {
final workerCommandPort = ReceivePort();
mainSendPort.send(workerCommandPort.sendPort);
SendPort? responsePort;
await for (final message in workerCommandPort) {
if (message is SendPort) {
responsePort = message;
} else if (message is (int, dynamic, dynamic)) {
final (taskId, Function fn, dynamic data) = message;
try {
final result = fn(data);
responsePort?.send((taskId, result, null));
} catch (e) {
responsePort?.send((taskId, null, e.toString()));
}
}
}
}
Future shutdown() async {
for (final port in _workerReceivePorts) {
port.close();
}
for (final isolate in _isolates) {
isolate.kill(priority: Isolate.immediate);
}
_pendingTasks.clear();
_isStarted = false;
print('[IsolatePool] Oprit curat.');
}
}
// Exemplu de consum în producție:
Future main() async {
final pool = IsolatePool(workerCount: 4);
await pool.start();
// Trimitem 10 sarcini intensive paralele către pool:
final futures = List.generate(10, (i) {
return pool.execute((data) {
// Simulare calcul CPU intensiv pe isolate separat:
int sum = 0;
for (int k = 0; k < 5000000; k++) {
sum += (k * data) % 997;
}
return sum;
}, i + 1);
});
final results = await Future.wait(futures);
print('Toate sarcinile s-au executat în paralel fără jank: $results');
await pool.shutdown();
}