Isolate & Concurrency #
Dart dirancang untuk berjalan pada satu thread per Isolate — tidak ada shared memory, tidak ada race condition, tidak ada mutex. Tapi ini tidak berarti Dart tidak bisa memanfaatkan banyak CPU core. Artikel ini membahas model concurrency Dart secara mendalam: bagaimana Isolate bekerja, kapan menggunakannya, dan pola-pola komunikasi yang perlu dikuasai.
Concurrency vs Parallelism #
Dua konsep yang sering tertukar namun berbeda secara fundamental:
CONCURRENCY (async/await, Event Loop):
Thread A: ──task1──┐ ┌──task1──┐ ┌──task1──>
└──task2───┘ └─task3──┘
(bergantian di satu thread -- terasa bersamaan, tapi tidak benar-benar)
PARALLELISM (Isolate):
Thread A: ──────────────── main isolate ──────────────────>
Thread B: ── worker isolate (CPU core berbeda) ──>
(benar-benar bersamaan -- dua CPU core berjalan simultan)
async/await memberikan concurrency — kode satu thread bergantian secara efisien. Isolate memberikan parallelism — kode benar-benar berjalan secara bersamaan di CPU core yang berbeda.
Mengapa Isolate Tidak Berbagi Memori? #
Model Isolate Dart berbeda dari thread di Java, C++, atau Go — di mana thread bisa mengakses variabel yang sama dan menyebabkan race condition:
Model Thread Tradisional (Java/C++):
Thread A Thread B
| |
+---> [Shared Memory] <---+
(data bersama)
Masalah: race condition, deadlock,
perlu mutex/lock yang kompleks
Model Isolate Dart:
Isolate A Isolate B
[Heap A] [Heap B]
| |
+---> [Port] <--+
(message passing)
Aman: tidak ada shared state,
tidak ada race condition
Karena setiap Isolate punya heap memori sendiri, kamu tidak bisa langsung berbagi variabel antar Isolate. Komunikasi hanya melalui message passing — mengirim salinan data melalui Port.
Isolate.run() — API Paling Sederhana #
Isolate.run() adalah cara termudah menjalankan komputasi berat di Isolate background. Ia membuat Isolate baru, menjalankan fungsi, mengembalikan hasilnya, lalu langsung menutup Isolate:
import 'dart:isolate';
// Fungsi yang MAHAL -- akan menyebabkan jank jika di UI thread
List<int> hitungBilangan Prima(int batas) {
final hasil = <int>[];
for (int n = 2; n <= batas; n++) {
bool prima = true;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) { prima = false; break; }
}
if (prima) hasil.add(n);
}
return hasil;
}
// Jalankan di Isolate background -- UI tetap responsive
Future<void> proses() async {
print('Mulai kalkulasi...');
// Isolate.run menunggu hingga selesai tapi tidak memblokir UI thread
final prima = await Isolate.run(() => hitungBilangan Prima(1000000));
print('Ditemukan ${prima.length} bilangan prima');
}
Isolate.run() vs compute() #
// compute() dari package:flutter/foundation.dart
// Ini adalah wrapper tipis di atas Isolate.run()
import 'package:flutter/foundation.dart';
// Keduanya ekuivalen -- pilih yang kamu suka
final result1 = await Isolate.run(() => beratKomputasi(data));
final result2 = await compute(beratKomputasi, data);
// Perbedaan kecil:
// compute() membutuhkan top-level atau static function
// Isolate.run() bisa menerima closure (lambda)
final result3 = await Isolate.run(() {
final processed = preprocess(data); // bisa akses local variable
return beratKomputasi(processed);
});
Kapan UI Thread Terasa “Jank” #
// MENYEBABKAN JANK -- blokir UI thread selama 300ms+
Future<void> loadDataSalah() async {
final rawJson = await http.read(url); // I/O: OK, async
final data = jsonDecode(rawJson); // CPU: OK, ringan
final parsed = parseRibuanItem(data); // CPU: BERAT! 300ms+ di UI thread
setState(() => _items = parsed); // JANK di frame sebelumnya
}
// TIDAK MENYEBABKAN JANK -- parsing di Isolate background
Future<void> loadDataBenar() async {
final rawJson = await http.read(url);
final parsed = await Isolate.run(() {
final data = jsonDecode(rawJson); // semua berjalan di worker isolate
return parseRibuanItem(data);
});
setState(() => _items = parsed);
}
Komunikasi Satu Arah dengan SendPort & ReceivePort #
Untuk kasus yang lebih kompleks dari Isolate.run(), kamu bisa menggunakan SendPort dan ReceivePort secara langsung.
Pola Dasar: Main → Worker → Main #
import 'dart:isolate';
// Fungsi yang berjalan di worker isolate
// HARUS top-level atau static -- tidak bisa closure biasa
void workerIsolate(SendPort mainSendPort) {
// Lakukan komputasi berat
int hasil = 0;
for (int i = 0; i < 1000000000; i++) {
hasil += i;
}
// Kirim hasil kembali ke main isolate
mainSendPort.send(hasil);
}
Future<void> main() async {
// Buat ReceivePort di main isolate untuk menerima pesan
final receivePort = ReceivePort();
// Spawn worker isolate, kirim SendPort sebagai "alamat balik"
await Isolate.spawn(workerIsolate, receivePort.sendPort);
// Tunggu satu pesan dari worker
final hasil = await receivePort.first;
print('Hasil: $hasil');
receivePort.close();
}
Long-Lived Worker Isolate — Komunikasi Dua Arah #
Pola yang lebih powerful: worker isolate yang bisa menerima banyak pesan dan merespons setiap saat — tanpa harus di-spawn ulang setiap kali:
import 'dart:isolate';
import 'dart:convert';
// Worker class yang mengelola long-lived isolate
class JsonWorker {
late SendPort _workerSendPort;
final _ready = Completer<void>();
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleFromWorker);
await Isolate.spawn(_workerEntry, receivePort.sendPort);
await _ready.future; // tunggu worker siap
}
// Handler untuk pesan dari worker
void _handleFromWorker(dynamic message) {
if (message is SendPort) {
// Worker mengirim SendPort-nya -- simpan untuk komunikasi
_workerSendPort = message;
_ready.complete();
} else if (message is Map<String, dynamic>) {
// Worker mengirim hasil parsing
print('Parsed: $message');
}
}
// Kirim data ke worker untuk diproses
Future<void> parseJson(String jsonString) async {
await _ready.future;
_workerSendPort.send(jsonString);
}
// Entry point yang berjalan di worker isolate
static void _workerEntry(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
// Kirim SendPort worker ke main -- siap menerima pesan
mainSendPort.send(workerReceivePort.sendPort);
// Dengarkan pesan dari main isolate
workerReceivePort.listen((message) {
if (message is String) {
// Parse JSON di worker isolate -- tidak blokir UI!
final parsed = jsonDecode(message) as Map<String, dynamic>;
mainSendPort.send(parsed); // kirim hasil ke main
}
});
}
}
// Penggunaan
Future<void> main() async {
final worker = JsonWorker();
await worker.spawn();
await worker.parseJson('{"nama": "Flutter", "versi": 3}');
await worker.parseJson('{"nama": "Dart", "versi": 3}');
}
RawReceivePort — Pattern untuk Startup yang Lebih Bersih #
Dokumentasi resmi Dart merekomendasikan RawReceivePort untuk memisahkan logika startup dari logika message handling:
import 'dart:isolate';
Future<(ReceivePort, SendPort)> spawnWorker() async {
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
// RawReceivePort hanya untuk menerima SendPort awal dari worker
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
try {
await Isolate.spawn(
_workerMain,
(initPort.sendPort),
);
} on Object {
initPort.close();
rethrow;
}
return connection.future;
}
void _workerMain(SendPort initSendPort) {
final commandPort = ReceivePort();
initSendPort.send(commandPort.sendPort); // kirim "alamat" worker
// Setelah ini, terima command dari main isolate
commandPort.listen((message) {
// proses command...
print('Worker menerima: $message');
});
}
TransferableTypedData — Transfer Data Besar Tanpa Copy #
Secara default, semua data yang dikirim antar Isolate di-copy — membuat salinan baru. Untuk data biner besar (gambar, audio, buffer), ini sangat mahal. TransferableTypedData memindahkan ownership data tanpa copy:
import 'dart:isolate';
import 'dart:typed_data';
// Tanpa TransferableTypedData: copy 10MB = 10MB alokasi baru
void workerBiasa(SendPort sendPort) {
final buffer = Uint8List(10 * 1024 * 1024); // 10MB
// ... isi buffer ...
sendPort.send(buffer); // COPY 10MB ke main isolate!
}
// Dengan TransferableTypedData: transfer ownership -- tidak ada copy
void workerEfisien(SendPort sendPort) {
final buffer = Uint8List(10 * 1024 * 1024); // 10MB
// ... isi buffer ...
final transferable = TransferableTypedData.fromList([buffer]);
sendPort.send(transferable); // TRANSFER ownership -- O(1)!
// buffer TIDAK BISA diakses lagi di worker setelah ini
}
Future<void> main() async {
final receivePort = ReceivePort();
await Isolate.spawn(workerEfisien, receivePort.sendPort);
receivePort.listen((message) {
if (message is TransferableTypedData) {
// Materialize di main isolate -- data ada di sini sekarang
final data = message.materialize().asUint8List();
print('Received ${data.length} bytes');
receivePort.close();
}
});
}
Isolate Groups — Berbagi Kode (Bukan Data) #
Dart mendukung konsep Isolate Groups di mana beberapa Isolate berbagi kode yang dikompilasi (program snapshot) tapi tetap punya heap memori yang terpisah. Ini membuat spawn lebih cepat karena tidak perlu reload kode dari awal:
// Isolate.spawn() secara default membuat Isolate dalam group yang sama
// Artinya kode yang dikompilasi di-share -- spawn lebih cepat
await Isolate.spawn(workerFunction, message);
// Untuk Isolate yang benar-benar terpisah (berbeda group):
await Isolate.spawnUri(
Uri.parse('worker.dart'), // file dart terpisah
[],
null,
);
Apa yang Bisa Dikirim Antar Isolate? #
Tidak semua objek bisa dikirim via SendPort.send(). Berikut yang didukung dan tidak:
BISA dikirim:
✓ Primitive types: null, bool, int, double, String
✓ List, Map, Set (berisi tipe yang bisa dikirim)
✓ TransferableTypedData
✓ SendPort
✓ Isolate
✓ Object tanpa native resources (plain Dart objects)
✓ Records (Dart 3)
TIDAK BISA dikirim:
✗ Closure / lambda
✗ ReceivePort
✗ Object dengan native resources (Socket, File handle)
✗ Mirrors
Panduan Praktis: Kapan Pakai Isolate? #
Tanda kamu PERLU Isolate:
✗ Animasi terasa patah saat parsing data
✗ UI freeze saat decode gambar besar
✗ Loading screen terasa "stuck" saat memproses response API besar
✗ Operasi enkripsi/dekripsi membuat UI tidak responsif
Use cases yang COCOK untuk Isolate:
✓ JSON parsing untuk dataset > 1000 item
✓ Image processing (resize, filter, compress)
✓ Enkripsi dan dekripsi data sensitif
✓ Komputasi matematika atau algoritma berat
✓ PDF generation atau parsing
✓ Text indexing untuk fitur search
Yang TIDAK perlu Isolate (cukup async/await):
✓ HTTP request -- sudah async, tidak blokir UI
✓ File I/O -- sudah async
✓ Database query -- sudah async
✓ SharedPreferences -- sudah async
Ringkasan #
- Dart menggunakan Isolate untuk parallelism sejati — setiap Isolate punya heap memori terpisah sehingga tidak ada race condition atau shared state.
Isolate.run()adalah cara termudah untuk menjalankan komputasi berat di background — buat Isolate, jalankan fungsi, kembalikan hasil, tutup.compute()dari Flutter adalah wrapper tipis di atasIsolate.run()— keduanya ekuivalen, pilih sesuai preferensi.- Untuk komunikasi yang lebih kompleks, gunakan
SendPortdanReceivePortsecara langsung — satu arah atau dua arah.- Long-lived worker Isolate berguna untuk task yang perlu dijalankan berulang sepanjang lifetime aplikasi tanpa overhead spawn berulang.
TransferableTypedDatamentransfer ownership data biner besar antar Isolate tanpa copy — sangat penting untuk operasi image processing.- Tidak semua objek bisa dikirim antar Isolate — closure, ReceivePort, dan objek dengan native resource tidak bisa dikirim via
SendPort.send().- Gunakan Isolate untuk operasi CPU-bound yang menyebabkan jank. Untuk I/O-bound (network, file, database),
async/awaitsudah cukup.
← Sebelumnya: Functional Programming Berikutnya: Best Practice →