Multi Threading

Multi Threading #

Dart adalah bahasa single-threaded — satu thread utama menangani semua kode UI, event pengguna, dan logika bisnis. Namun ini bukan berarti Dart tidak bisa melakukan banyak hal sekaligus. Dart memisahkan dua konsep yang sering dicampuradukkan: concurrency (menangani banyak tugas secara bergantian di satu thread, via async/await) dan parallelism (menjalankan kode di beberapa CPU core secara sungguhan bersamaan, via Isolate). Memahami kapan masing-masing dibutuhkan — dan mengapa async/await sudah cukup untuk sebagian besar kasus — adalah keterampilan inti dalam menulis aplikasi Dart yang responsif dan efisien.

Event Loop — Pondasi Concurrency Dart #

Sebelum membahas Isolate, penting memahami mengapa Dart bisa “terasa” multithreaded padahal single-threaded. Dart menggunakan event loop — mekanisme yang memproses antrian event satu per satu, tapi dalam urutan yang memungkinkan banyak operasi I/O berjalan “secara bersamaan”:

flowchart TD
    A["Kode Dart\n(satu thread)"] --> B["Event Loop"]
    B --> C{"Ada event?"}
    C -- Microtask --> D["Proses microtask\n(Future.value, scheduleMicrotask)"]
    C -- Event --> E["Proses event\n(Timer, I/O callback, UI event)"]
    D --> C
    E --> C
    B --> F["Antrian Microtask\n(prioritas lebih tinggi)"]
    B --> G["Antrian Event\n(Timer, I/O, user input)"]
void main() async {
  print('1 — sinkron');

  Future.microtask(() => print('2 — microtask'));

  Future.value('3').then((v) => print(v));

  Timer(Duration.zero, () => print('4 — event queue'));

  await Future.delayed(Duration(seconds: 1));

  print('5 — setelah delay');
}

// Output:
// 1 — sinkron
// 2 — microtask
// 3
// 4 — event queue
// 5 — setelah delay

Intinya: async/await dan Future menggunakan event loop untuk concurrency — operasi I/O (HTTP, file, database) bisa menunggu tanpa memblokir thread. Namun, komputasi berat (CPU-bound) tetap memblokir thread karena kode Dart sungguhan hanya bisa berjalan satu per satu.


Async/Await Sudah Cukup — Kapan Tidak Perlu Isolate #

Ini adalah keputusan yang paling sering salah: developer menggunakan Isolate untuk masalah yang sebenarnya bisa diselesaikan dengan async/await.

// TIDAK butuh Isolate — I/O bound, bisa diselesaikan dengan async/await
Future<List<Produk>> ambilProduk() async {
  final response = await http.get(Uri.parse('/api/produk')); // menunggu jaringan
  return jsonDecode(response.body)
      .map<Produk>((j) => Produk.fromJson(j))
      .toList();
}

// TIDAK butuh Isolate — baca file, bisa async
Future<String> bacaFile(String path) async {
  return await File(path).readAsString(); // menunggu disk I/O
}

// BUTUH Isolate — CPU-bound, memblokir thread jika tidak diisolasi
List<int> sortDataBesar(List<int> data) {
  data.sort(); // Jika data 10 juta elemen — memblokir thread beberapa detik!
  return data;
}

// BUTUH Isolate — parsing/komputasi berat
Map<String, dynamic> parseJsonBesar(String jsonString) {
  return jsonDecode(jsonString); // JSON 50MB bisa memblokir thread > 1 detik
}
GUNAKAN async/await (tidak perlu Isolate) ketika:
  ✓ Operasi I/O: HTTP, file, database, socket
  ✓ Operasi yang menunggu: Timer, delay, user input
  ✓ Komputasi ringan di antara event

GUNAKAN Isolate ketika:
  ✓ Komputasi CPU-bound yang berat (> ~16ms untuk 60fps UI)
  ✓ Parsing file/JSON yang sangat besar
  ✓ Image processing, enkripsi, kompresi
  ✓ Machine learning inference
  ✓ Operasi yang bisa dipecah dan diparalelkan

Isolate.run — API Modern (Dart 2.19+) #

Cara termudah menjalankan komputasi berat di Isolate terpisah. Isolate.run membuat Isolate baru, menjalankan fungsi, mengembalikan hasilnya, lalu otomatis membersihkan Isolate:

import 'dart:isolate';

// Fungsi yang akan dijalankan di Isolate terpisah
// HARUS top-level atau static — tidak bisa closure yang capture variabel luar
int hitungFibonacci(int n) {
  if (n <= 1) return n;
  return hitungFibonacci(n - 1) + hitungFibonacci(n - 2);
}

void main() async {
  print('Mulai komputasi...');

  // Isolate.run — jalankan di background, await hasilnya
  final hasil = await Isolate.run(() => hitungFibonacci(40));

  print('Fibonacci(40) = $hasil'); // 102334155
  print('UI tetap responsif selama komputasi!');
}

Mengirim Data ke Isolate.run #

Data dikirim melalui closure atau parameter fungsi. Dart meng-copy data ke Isolate baru:

Future<List<Produk>> prosesDataBesar(List<Map<String, dynamic>> rawData) async {
  // Kirim rawData ke Isolate, proses di sana, kembalikan hasilnya
  return await Isolate.run(() {
    // rawData di-copy ke Isolate ini — tidak ada sharing memori
    return rawData
        .map(Produk.fromJson)
        .where((p) => p.stok > 0)
        .toList();
  });
}

// Dengan parameter eksplisit — lebih jelas
Future<String> kompresString(String input) async {
  return await Isolate.run(() {
    // Simulasi kompresi berat
    return _algoritmKompresi(input);
  });
}
// ANTI-PATTERN: `Isolate.run` untuk operasi I/O — tidak efisien
// I/O sudah non-blocking di Dart, Isolate overhead tidak perlu
final result = await Isolate.run(() async {
  return await http.get(Uri.parse('/api/data')); // ✗ overhead tanpa manfaat
});

// BENAR: langsung await untuk I/O
final result = await http.get(Uri.parse('/api/data')); // ✓

Isolate.spawn — Kontrol Penuh #

Untuk kasus yang lebih kompleks — komunikasi dua arah, Isolate yang persisten, atau mengirim banyak pesan — gunakan Isolate.spawn dengan SendPort/ReceivePort:

Komunikasi Satu Arah (Sederhana) #

import 'dart:isolate';

// Entry point Isolate HARUS top-level function
void entryPoint(SendPort kirimKe) {
  // Jalankan pekerjaan
  final hasil = hitungSesuatuYangBerat();
  // Kirim hasil balik ke main isolate
  kirimKe.send(hasil);
}

void main() async {
  // Buat ReceivePort untuk menerima pesan dari Isolate baru
  final terima = ReceivePort();

  // Spawn Isolate, kirimkan SendPort agar bisa balas
  final isolate = await Isolate.spawn(entryPoint, terima.sendPort);

  // Tunggu satu pesan
  final hasil = await terima.first;
  print('Hasil: $hasil');

  // Bersihkan
  terima.close();
  isolate.kill();
}

Komunikasi Dua Arah — Pola Umum #

Untuk komunikasi dua arah yang lengkap, Isolate baru perlu mengirimkan SendPort-nya sendiri kembali ke main isolate:

import 'dart:isolate';

// Pesan untuk inisialisasi
class InitPesan {
  final SendPort kirimKe;
  const InitPesan(this.kirimKe);
}

// Worker Isolate — menerima pekerjaan dan mengirim hasil
void workerEntryPoint(SendPort kirimKeMain) {
  // Buat ReceivePort untuk menerima perintah dari main
  final terimaPerintah = ReceivePort();

  // Kirim SendPort kita ke main agar main bisa kirim perintah
  kirimKeMain.send(terimaPerintah.sendPort);

  // Dengarkan perintah dari main
  terimaPerintah.listen((pesan) {
    if (pesan is int) {
      // Proses pekerjaan
      final hasil = hitungFibonacci(pesan);
      kirimKeMain.send(hasil);
    } else if (pesan == 'tutup') {
      terimaPerintah.close();
    }
  });
}

void main() async {
  final terimaMain = ReceivePort();

  // Spawn worker isolate
  await Isolate.spawn(workerEntryPoint, terimaMain.sendPort);

  // Terima SendPort dari worker
  final SendPort kirimKeWorker = await terimaMain.first;

  // Kirim pekerjaan ke worker
  kirimKeWorker.send(40);  // hitung fibonacci(40)
  final hasil1 = await terimaMain.first;
  print('Fibonacci(40) = $hasil1');

  kirimKeWorker.send(35);  // kerjaan berikutnya
  final hasil2 = await terimaMain.first;
  print('Fibonacci(35) = $hasil2');

  // Selesai
  kirimKeWorker.send('tutup');
  terimaMain.close();
}

Transfer Data Antar Isolate #

Karena Isolate tidak berbagi memori, data harus ditransfer melalui pesan. Dart menggunakan dua strategi:

Copy — Untuk Data Kecil #

Data di-copy dari satu Isolate ke Isolate lain. Keduanya punya salinan independen:

// Semua tipe primitif dan koleksi standar di-copy
isolate.send(42);                    // int — di-copy
isolate.send('halo');               // String — di-copy
isolate.send([1, 2, 3]);           // List — di-copy (deep copy)
isolate.send({'a': 1});            // Map — di-copy (deep copy)

Transfer — Untuk Data Besar (Zero-Copy) #

Beberapa tipe bisa ditransfer tanpa copy — objek dipindahkan ke Isolate penerima dan tidak lagi bisa diakses dari Isolate pengirim:

import 'dart:typed_data';
import 'dart:isolate';

// TransferableTypedData — untuk data biner besar (gambar, audio, dll)
// Zero-copy transfer — sangat efisien untuk data besar
Future<void> prosesGambar(Uint8List pixelData) async {
  // Bungkus sebagai TransferableTypedData untuk zero-copy
  final transferable = TransferableTypedData.fromList([pixelData]);

  final terima = ReceivePort();
  await Isolate.spawn(
    _prosesGambarEntryPoint,
    [terima.sendPort, transferable],
  );

  final hasilPixel = await terima.first as Uint8List;
  terima.close();
  return hasilPixel;
}

void _prosesGambarEntryPoint(List<dynamic> args) {
  final SendPort kirimKe = args[0];
  final TransferableTypedData data = args[1];

  // Materialize kembali menjadi Uint8List
  final pixels = data.materialize().asUint8List();

  // Proses gambar...
  final hasilPixel = _filterGrayscale(pixels);

  kirimKe.send(hasilPixel);
}
// ANTI-PATTERN: kirim objek kelas kustom yang tidak bisa di-serialize
class Pengguna {
  final String nama;
  final Stream<int> stream; // ✗ Stream tidak bisa dikirim antar Isolate
  Pengguna(this.nama, this.stream);
}

// Isolate akan throw: Illegal argument in isolate message
isolate.send(Pengguna('Budi', stream));

// BENAR: kirim data yang bisa di-serialize (primitif, Map, List)
isolate.send({'nama': 'Budi', 'umur': 25}); // ✓ Map di-copy
isolate.send(pengguna.toJson());             // ✓ konversi ke Map dulu

Isolate Pool — Penggunaan Berulang #

Membuat Isolate baru untuk setiap tugas punya overhead yang signifikan. Untuk tugas yang sering, buat pool Isolate yang siap menerima pekerjaan:

import 'dart:isolate';
import 'dart:async';

class IsolatePool {
  final int ukuran;
  final _workers = <_Worker>[];
  int _indeksBerikutnya = 0;

  IsolatePool({this.ukuran = 4});

  Future<void> inisialisasi() async {
    for (int i = 0; i < ukuran; i++) {
      final worker = _Worker();
      await worker.mulai();
      _workers.add(worker);
    }
  }

  Future<T> jalankan<T>(dynamic Function() tugas) {
    // Round-robin assignment ke worker
    final worker = _workers[_indeksBerikutnya];
    _indeksBerikutnya = (_indeksBerikutnya + 1) % ukuran;
    return worker.jalankan<T>(tugas);
  }

  void tutup() {
    for (final w in _workers) w.tutup();
  }
}

class _Worker {
  late Isolate _isolate;
  late SendPort _kirimKe;
  final _tugas = <Completer>{};

  Future<void> mulai() async {
    final terima = ReceivePort();
    _isolate = await Isolate.spawn(_workerLoop, terima.sendPort);
    _kirimKe = await terima.first;
    // Setup listener untuk hasil...
  }

  Future<T> jalankan<T>(dynamic Function() tugas) {
    final completer = Completer<T>();
    _kirimKe.send(tugas);
    // Simpan completer untuk di-resolve saat hasil datang
    return completer.future;
  }

  void tutup() => _isolate.kill();
}

compute di Flutter — Isolate yang Disederhanakan #

Flutter menyediakan fungsi compute yang membungkus Isolate.run dengan API yang lebih sederhana. Ini adalah cara yang paling idiomatis di Flutter untuk memindahkan komputasi berat ke background:

import 'package:flutter/foundation.dart';

// Fungsi harus top-level atau static
List<Produk> _parseJsonDiBackground(String jsonString) {
  final data = jsonDecode(jsonString) as List;
  return data.map((j) => Produk.fromJson(j as Map<String, dynamic>)).toList();
}

// Di dalam widget atau provider
Future<void> muatProduk() async {
  final jsonString = await ambilJsonDariApi();

  // compute() — jalankan parsing di Isolate terpisah
  // UI tetap smooth selama parsing berlangsung
  final produk = await compute(_parseJsonDiBackground, jsonString);

  setState(() => _produk = produk);
}
// ANTI-PATTERN: parsing JSON besar di main isolate — memblokir UI
Future<void> muatProduk() async {
  final response = await http.get(Uri.parse('/api/produk'));
  // ✗ jsonDecode 10MB di main thread = UI freeze beberapa detik
  final produk = (jsonDecode(response.body) as List)
      .map<Produk>((j) => Produk.fromJson(j))
      .toList();
  setState(() => _produk = produk);
}

// BENAR: pindahkan ke background dengan compute
Future<void> muatProduk() async {
  final response = await http.get(Uri.parse('/api/produk'));
  // ✓ parsing di background — UI tidak freeze
  final produk = await compute(_parseProduk, response.body);
  setState(() => _produk = produk);
}

// Harus top-level function
List<Produk> _parseProduk(String json) {
  return (jsonDecode(json) as List).map<Produk>(Produk.fromJson).toList();
}

Keterbatasan Isolate yang Perlu Dipahami #

// 1. Tidak bisa kirim closure yang capture variabel luar
int multiplier = 3;
await Isolate.run(() => 10 * multiplier); // ✗ bisa bermasalah

// 2. Tidak bisa kirim objek dengan native resource (socket, file handle)
final socket = await Socket.connect('localhost', 8080);
isolate.send(socket); // ✗ tidak bisa — native resource tidak bisa di-transfer

// 3. Tidak bisa kirim fungsi (kecuali sebagai top-level reference)
isolate.send((x) => x * 2); // ✗ closure tidak bisa dikirim
isolate.send(hitungFibonacci); // ✓ top-level function reference bisa

// 4. Overhead spawn — jangan spawn Isolate untuk tugas yang sangat singkat
await Isolate.run(() => 1 + 1); // ✗ overhead spawn jauh lebih besar dari komputasinya

// 5. Tidak ada shared state — data harus dikirim eksplisit
// Tidak ada variabel global yang bisa diakses lintas Isolate

Perbandingan: async/await vs Isolate #

flowchart TD
    A{"Tugas apa yang<br/>perlu dilakukan?"} --> B{"I/O bound?<br/>HTTP, file, DB"}
    B -- Ya --> C["async/await<br/>Future, Stream"]
    B -- Tidak --> D{"CPU bound?<br/>Menggunakan &gt; ~16ms"}
    D -- Tidak --> E["async/await<br/>sudah cukup"]
    D -- Ya --> F{"Dart 2.19+?"}
    F -- Ya --> G["Isolate.run<br/>atau compute Flutter"]
    F -- Tidak --> H["Isolate.spawn<br/>+ SendPort/ReceivePort"]
    G --> I{"Perlu persistent<br/>Isolate / komunikasi<br/>2 arah?"}
    I -- Ya --> H
    I -- Tidak --> G
Aspek async/await Isolate
Model memori Shared (satu thread) Terpisah (tidak shared)
Overhead Sangat rendah Tinggi (spawn + copy data)
Cocok untuk I/O, event, UI Komputasi CPU berat
Komunikasi Langsung via variabel Message passing
Bisa share objek ✗ (hanya transfer/copy)
Blokir UI? ✓ jika CPU-bound ✗ berjalan di core lain

Ringkasan #

  • Dart adalah single-threaded, tapi event loop memungkinkan banyak operasi I/O berjalan “bersamaan” via async/await — tanpa blocking thread utama.
  • async/await sudah cukup untuk semua I/O — HTTP, file, database, socket. Isolate bukan solusi untuk masalah I/O yang lambat.
  • Gunakan Isolate untuk CPU-bound — komputasi yang memakan waktu > 16ms: parsing data besar, enkripsi, kompresi, image processing, ML inference.
  • Isolate.run (Dart 2.19+) adalah cara termudah menjalankan komputasi di background — buat Isolate, jalankan fungsi, kembalikan hasil, bersihkan otomatis.
  • compute di Flutter adalah wrapper Isolate.run yang lebih idiomatis — gunakan ini sebagai default untuk memindahkan parsing/komputasi berat dari UI thread.
  • Isolate tidak berbagi memori — semua data harus dikirim melalui pesan. Tipe primitif dan koleksi standar di-copy; TransferableTypedData untuk transfer zero-copy data besar.
  • Tidak semua objek bisa dikirim — closure, socket, file handle, dan objek dengan native resource tidak bisa dikirim antar Isolate.
  • Entry point Isolate harus top-level atau static — tidak bisa menggunakan closure yang menangkap variabel dari scope luar.
  • Hindari spawn Isolate untuk tugas singkat — overhead spawn bisa lebih besar dari komputasinya sendiri. Untuk tugas yang sering, pertimbangkan Isolate pool.
  • Isolate.spawn untuk komunikasi dua arah — ketika perlu Isolate persisten yang menerima banyak perintah dan mengirim banyak hasil.

← Sebelumnya: Pub.dev   Berikutnya: I/O →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact