Dart Isolates i Współbieżność w 2026: compute, Async oraz Pytania Rekrutacyjne

Kompleksowy przewodnik po izolatkach Dart, funkcji compute i wzorcach async dla aplikacji Flutter. Omówienie modeli współbieżności, optymalizacji wydajności oraz typowych pytań rekrutacyjnych z przykładami kodu.

Wizualizacja współbieżności izolatek Dart z równoległymi wątkami i izolowanymi przepływami wykonania

Izolatki Dart zapewniają prawdziwą równoległą egzekucję w aplikacjach Flutter, rozwiązując problem jednowątkowych ograniczeń powodujących zacinanie się interfejsu użytkownika podczas intensywnych obliczeń. W przeciwieństwie do wątków w innych językach, izolatki nie współdzielą pamięci i komunikują się wyłącznie poprzez przekazywanie wiadomości, co sprawia, że kod współbieżny jest bezpieczniejszy i łatwiejszy do zrozumienia.

Kluczowe Informacje na 2026

Dart 3.7 wprowadził lekkie izolatki z 50% zmniejszonym zużyciem pamięci. API Isolate.run() obsługuje teraz większość przypadków użycia, które wcześniej wymagały ręcznego tworzenia izolatek, podczas gdy compute() pozostaje preferowaną opcją dla zadań specyficznych dla Flutter.

Zrozumienie Modelu Współbieżności Dart

Dart wykonuje kod w jednowątkowej pętli zdarzeń wewnątrz każdej izolatki. Oznacza to, że async/await nie zapewnia równoległości — pozwala jedynie na nieblokujące operacje I/O. Gdy zadanie wymagające intensywnych obliczeń CPU uruchamia się na głównej izolatce, interfejs użytkownika zamraża się, ponieważ pętla zdarzeń nie może przetwarzać wywołań zwrotnych ramek.

Rozwiązaniem jest przeniesienie ciężkiej pracy do oddzielnych izolatek. Każda izolatka ma własną stertę pamięci i pętlę zdarzeń, działającą na osobnym wątku zarządzanym przez maszynę wirtualną Dart. Komunikacja odbywa się poprzez SendPort i ReceivePort, które serializują wiadomości między izolatkami.

main.dartdart
import 'dart:isolate';

// Heavy computation that would block the UI if run on main isolate
int computeFibonacci(int n) {
  if (n <= 1) return n;
  return computeFibonacci(n - 1) + computeFibonacci(n - 2);
}

Future<void> main() async {
  // Isolate.run() handles spawning, message passing, and cleanup automatically
  final result = await Isolate.run(() => computeFibonacci(40));
  print('Fibonacci(40) = $result'); // Computed without blocking main thread
}

Funkcja Isolate.run() wprowadzona w Dart 2.19 upraszcza typowy wzorzec uruchamiania pojedynczego obliczenia w izolatce działającej w tle. Automatycznie obsługuje tworzenie portów, serializację wiadomości i usuwanie izolatki.

Używanie compute() w Aplikacjach Flutter

Flutter dostarcza funkcję compute() zaprojektowaną specjalnie do przenoszenia pracy z izolatki UI. W przeciwieństwie do surowych API izolatek, compute() integruje się z frameworkiem Flutter i obsługuje standardowy kod zarządzania izolatkami.

image_processor.dartdart
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;

// Top-level function required for compute() - cannot be a closure or method
Uint8List _applyGrayscaleFilter(Uint8List imageBytes) {
  final image = img.decodeImage(imageBytes)!;
  final grayscale = img.grayscale(image);
  return Uint8List.fromList(img.encodePng(grayscale));
}

class ImageProcessor {
  Future<Uint8List> processImage(Uint8List rawImage) async {
    // compute() spawns isolate, runs function, returns result, disposes isolate
    return await compute(_applyGrayscaleFilter, rawImage);
  }
}

Funkcja przekazywana do compute() musi być funkcją najwyższego poziomu lub metodą statyczną. Metody instancji i domknięcia przechwytują stan, który nie może być serializowany między granicami izolatek.

Częsty Błąd

Przekazywanie obiektów niemożliwych do serializacji do izolatek powoduje błędy w czasie wykonywania. Klasy muszą być prostymi obiektami danych — bez BuildContext, bez referencji do Stream, bez singletonów. Należy wyodrębnić tylko prymitywne dane potrzebne do obliczeń.

Dwukierunkowa Komunikacja z Portami

Złożone scenariusze wymagają ciągłej komunikacji między izolatkami zamiast pojedynczego cyklu żądanie-odpowiedź. Długotrwałe zadania w tle, przetwarzanie strumieniowe danych i pule workerów wymagają dwukierunkowego przekazywania wiadomości.

background_worker.dartdart
import 'dart:isolate';
import 'dart:async';

class BackgroundWorker {
  late Isolate _isolate;
  late SendPort _sendPort;
  final _responseController = StreamController<dynamic>.broadcast();
  
  Stream<dynamic> get responses => _responseController.stream;

  Future<void> start() async {
    final receivePort = ReceivePort();
    
    // Spawn isolate with initial SendPort for bidirectional setup
    _isolate = await Isolate.spawn(_workerEntryPoint, receivePort.sendPort);
    
    // First message from worker contains its SendPort
    final completer = Completer<SendPort>();
    receivePort.listen((message) {
      if (message is SendPort) {
        completer.complete(message);
      } else {
        _responseController.add(message); // Forward results to stream
      }
    });
    
    _sendPort = await completer.future;
  }

  void sendTask(Map<String, dynamic> task) {
    _sendPort.send(task);
  }

  void dispose() {
    _isolate.kill(priority: Isolate.immediate);
    _responseController.close();
  }
}

// Entry point runs in the background isolate
void _workerEntryPoint(SendPort mainSendPort) {
  final workerReceivePort = ReceivePort();
  mainSendPort.send(workerReceivePort.sendPort); // Send port back to main
  
  workerReceivePort.listen((message) {
    final task = message as Map<String, dynamic>;
    final result = _processTask(task); // Heavy computation here
    mainSendPort.send(result);
  });
}

dynamic _processTask(Map<String, dynamic> task) {
  // Simulate CPU-intensive work
  return {'taskId': task['id'], 'processed': true};
}

Ten wzorzec ustanawia trwałą izolatkę workera, która akceptuje wiele zadań w czasie. Główna izolatka wysyła zadania przez SendPort, a worker odpowiada przez własny port. Stan wewnątrz izolatki workera utrzymuje się między zadaniami, umożliwiając buforowanie i przyrostowe przetwarzanie.

Async/Await vs Izolatki: Wybór Właściwego Narzędzia

Częstym pytaniem rekrutacyjnym dla programistów Flutter jest kiedy używać async/await, a kiedy izolatek. To rozróżnienie ma znaczenie dla wydajności aplikacji.

| Scenariusz | Użyj async/await | Użyj izolatek | |----------|-----------------|-------------| | Żądania sieciowe | Tak | Nie | | I/O plików | Tak | Nie | | Parsowanie JSON (małe) | Tak | Nie | | Parsowanie JSON (>1MB) | Nie | Tak | | Przetwarzanie obrazów | Nie | Tak | | Operacje kryptograficzne | Nie | Tak | | Zapytania bazodanowe | Tak | Czasami | | Złożone obliczenia | Nie | Tak |

Async/await efektywnie obsługuje operacje związane z I/O, ponieważ maszyna wirtualna Dart deleguje oczekiwanie do systemu operacyjnego. Pętla zdarzeń pozostaje wolna do przetwarzania zdarzeń UI. Izolatki rozwiązują problemy związane z CPU, gdzie samo obliczenie zajmuje czas niezależnie od I/O.

json_service.dartdart
import 'dart:convert';
import 'package:flutter/foundation.dart';

class JsonService {
  // Small payloads: async is sufficient, isolate overhead not justified
  Future<Map<String, dynamic>> parseSmallJson(String json) async {
    return jsonDecode(json) as Map<String, dynamic>;
  }

  // Large payloads: isolate prevents UI jank during parsing
  Future<List<dynamic>> parseLargeJson(String json) async {
    return await compute(_parseJsonList, json);
  }
}

List<dynamic> _parseJsonList(String json) {
  return jsonDecode(json) as List<dynamic>;
}

Gotowy na rozmowy o Flutter?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Obsługa Błędów Między Granicami Izolatek

Błędy rzucane w izolatkach nie propagują się automatycznie do izolatki wywołującej. Bez odpowiedniej obsługi, wyjątki znikają po cichu, powodując trudne do debugowania problemy w aplikacjach produkcyjnych.

safe_isolate.dartdart
import 'dart:isolate';

class IsolateResult<T> {
  final T? value;
  final Object? error;
  final StackTrace? stackTrace;
  
  IsolateResult.success(this.value) : error = null, stackTrace = null;
  IsolateResult.failure(this.error, this.stackTrace) : value = null;
  
  bool get isSuccess => error == null;
}

Future<IsolateResult<T>> runSafeIsolate<T>(T Function() computation) async {
  try {
    final result = await Isolate.run(() {
      try {
        return IsolateResult<T>.success(computation());
      } catch (e, stack) {
        return IsolateResult<T>.failure(e, stack);
      }
    });
    return result;
  } catch (e, stack) {
    // Handle isolate spawn failures
    return IsolateResult<T>.failure(e, stack);
  }
}

// Usage
Future<void> processData() async {
  final result = await runSafeIsolate(() => riskyComputation());
  
  if (result.isSuccess) {
    print('Result: ${result.value}');
  } else {
    print('Error: ${result.error}');
    print('Stack: ${result.stackTrace}');
  }
}

int riskyComputation() {
  throw Exception('Something went wrong');
}

Wzorzec opakowania przechwytuje wyjątki wewnątrz izolatki i zwraca je jako dane, zapewniając że błędy ujawniają się w kodzie wywołującym. To podejście odzwierciedla wzorzec Result używany w Rust i jest zgodne z praktykami funkcyjnej obsługi błędów.

Optymalizacja Wydajności z Pulami Izolatek

Tworzenie izolatek wiąże się z narzutem — alokacją pamięci, kompilacją JIT kodu w nowej izolatce i serializacją wiadomości. Dla aplikacji przetwarzających wiele niezależnych zadań, ponowne wykorzystanie izolatek poprzez pulę poprawia przepustowość.

isolate_pool.dartdart
import 'dart:async';
import 'dart:collection';
import 'dart:isolate';

class IsolatePool {
  final int size;
  final Queue<_PooledIsolate> _available = Queue();
  final List<_PooledIsolate> _all = [];
  bool _initialized = false;

  IsolatePool({this.size = 4});

  Future<void> initialize() async {
    if (_initialized) return;
    
    for (int i = 0; i < size; i++) {
      final pooled = await _PooledIsolate.spawn();
      _all.add(pooled);
      _available.add(pooled);
    }
    _initialized = true;
  }

  Future<R> execute<R>(R Function() task) async {
    if (!_initialized) await initialize();
    
    // Wait for available isolate
    while (_available.isEmpty) {
      await Future.delayed(const Duration(milliseconds: 10));
    }
    
    final isolate = _available.removeFirst();
    try {
      return await isolate.run(task);
    } finally {
      _available.add(isolate); // Return to pool
    }
  }

  Future<void> dispose() async {
    for (final isolate in _all) {
      isolate.dispose();
    }
    _all.clear();
    _available.clear();
  }
}

class _PooledIsolate {
  final Isolate isolate;
  final SendPort sendPort;
  final ReceivePort receivePort;

  _PooledIsolate._(this.isolate, this.sendPort, this.receivePort);

  static Future<_PooledIsolate> spawn() async {
    final receivePort = ReceivePort();
    final isolate = await Isolate.spawn(_poolWorker, receivePort.sendPort);
    final sendPort = await receivePort.first as SendPort;
    return _PooledIsolate._(isolate, sendPort, receivePort);
  }

  Future<R> run<R>(R Function() task) async {
    final responsePort = ReceivePort();
    sendPort.send([task, responsePort.sendPort]);
    return await responsePort.first as R;
  }

  void dispose() {
    isolate.kill();
    receivePort.close();
  }
}

void _poolWorker(SendPort mainPort) {
  final workerPort = ReceivePort();
  mainPort.send(workerPort.sendPort);
  
  workerPort.listen((message) {
    final task = message[0] as Function;
    final replyPort = message[1] as SendPort;
    replyPort.send(task());
  });
}

Pula utrzymuje stałą liczbę rozgrzanych izolatek gotowych do przyjęcia pracy. Zadania kolejkują się, gdy wszystkie izolatki są zajęte, a ukończone izolatki natychmiast wracają do dostępnej puli.

Typowe Pytania Rekrutacyjne o Współbieżność Dart

Rozmowy techniczne często badają zrozumienie modelu współbieżności Dart. Przygotowanie do tych pytań demonstruje głębię wykraczającą poza podstawową wiedzę o widgetach Flutter. Moduł pytań rekrutacyjnych Flutter obejmuje dodatkowe tematy dotyczące widgetów i zarządzania stanem.

P: Co się stanie, jeśli wywołasz setState() z izolatki?

Wywołanie setState() z izolatki działającej w tle nie powiedzie się, ponieważ BuildContext i stan widgetu istnieją tylko w pamięci głównej izolatki. Izolatka nie może bezpośrednio uzyskać dostępu do obiektów głównej izolatki. Zamiast tego należy wysłać obliczony wynik z powrotem do głównej izolatki i wywołać setState() tam:

counter_page.dartdart
import 'dart:isolate';
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _result = 0;
  bool _computing = false;

  Future<void> _computeInBackground() async {
    setState(() => _computing = true);
    
    // Computation runs in isolate
    final result = await Isolate.run(() => _heavyCalculation(1000000));
    
    // setState called on main isolate after receiving result
    setState(() {
      _result = result;
      _computing = false;
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Result: $_result'),
        ElevatedButton(
          onPressed: _computing ? null : _computeInBackground,
          child: _computing 
              ? const CircularProgressIndicator() 
              : const Text('Compute'),
        ),
      ],
    );
  }
}

int _heavyCalculation(int iterations) {
  int sum = 0;
  for (int i = 0; i < iterations; i++) {
    sum += i;
  }
  return sum;
}

P: Czy izolatki mogą współdzielić obiekty?

Izolatki nie mogą współdzielić zmiennych obiektów. Każda izolatka ma własną stertę, a obiekty przekazywane między izolatkami są serializowane i kopiowane. Niezmienne obiekty i prymitywy mogą być przekazywane efektywnie, ponieważ Dart optymalizuje ich transfer. Zmienny stan wymaga jawnej synchronizacji poprzez przekazywanie wiadomości.

P: Jak async/await działa z izolatkami?

Async/await wewnątrz izolatki działa identycznie jak w głównej izolatce — każda izolatka ma własną pętlę zdarzeń. Oczekiwanie na Future w izolatce nie blokuje pętli zdarzeń głównej izolatki. Sama funkcja Isolate.run() zwraca Future, który kończy się po zakończeniu izolatki, integrując się płynnie z kodem asynchronicznym w głównej izolatce.

Debugowanie Kodu Opartego na Izolatkach

Debugowanie aplikacji wieloizolatkowych wymaga zrozumienia, że punkty przerwania i logowanie zachowują się inaczej między granicami izolatek.

debug_utils.dartdart
import 'dart:developer' as developer;
import 'dart:isolate';

void debugPrint(String message) {
  // developer.log works across isolates and appears in DevTools
  developer.log(
    message,
    name: 'IsolateDebug',
    time: DateTime.now(),
  );
}

Future<void> debuggableComputation() async {
  await Isolate.run(() {
    debugPrint('Starting computation in isolate');
    
    // Timeline events visible in DevTools performance tab
    developer.Timeline.startSync('HeavyWork');
    final result = performHeavyWork();
    developer.Timeline.finishSync();
    
    debugPrint('Computation complete: $result');
    return result;
  });
}

int performHeavyWork() {
  int sum = 0;
  for (int i = 0; i < 10000000; i++) {
    sum += i;
  }
  return sum;
}

Widok wydajności Flutter DevTools pokazuje zdarzenia osi czasu ze wszystkich izolatek, umożliwiając wizualizację równoległego wykonania i identyfikację wąskich gardeł.

Wskazówka DevTools

Profiler CPU w DevTools pomaga zidentyfikować, które izolatki zużywają najwięcej czasu przetwarzania. Wykres płomieniowy rozdziela wykonanie według izolatki, ujawniając czy praca jest właściwie rozłożona.

Podsumowanie

  • Izolatki zapewniają prawdziwą równoległość w Dart, działając na oddzielnych wątkach z izolowanymi stertami pamięci
  • Używaj Isolate.run() dla prostych jednorazowych obliczeń, compute() dla zadań specyficznych dla Flutter
  • Async/await obsługuje pracę związaną z I/O; izolatki rozwiązują problemy związane z CPU, które blokują pętlę zdarzeń
  • Funkcje przekazywane do izolatek muszą być najwyższego poziomu lub statyczne — domknięcia i metody instancji nie przechodzą serializacji
  • Implementuj obsługę błędów wewnątrz izolatek i zwracaj błędy jako dane, aby zapewnić właściwe ujawnienie wyjątków
  • Pule izolatek zmniejszają narzut tworzenia dla aplikacji przetwarzających wiele niezależnych zadań
  • Moduł zarządzania stanem Flutter uzupełnia wiedzę o współbieżności w przygotowaniu do rozmów rekrutacyjnych

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#dart
#flutter
#isolates
#concurrency
#async
#performance

Udostępnij

Powiązane artykuły