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 > ~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/awaitsudah 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.computedi Flutter adalah wrapperIsolate.runyang 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;
TransferableTypedDatauntuk 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.spawnuntuk komunikasi dua arah — ketika perlu Isolate persisten yang menerima banyak perintah dan mengirim banyak hasil.