Isolate

Isolate sebagai konkurensi tanpa memori bersama yang dapat diubah.

Daftar isi

async dan await mengatur pekerjaan yang menunggu, tetapi keduanya tetap berjalan pada satu alur eksekusi yang sama sehingga komputasi berat tetap menyita waktu di alur utama. Dart menjawab kebutuhan penggunaan lebih dari satu inti prosesor dengan isolate, yaitu unit pekerjaan yang masing-masing memegang memori dan event loop sendiri. Topik ini membahas Isolate.run untuk komputasi pendek, Isolate.spawn beserta SendPort dan ReceivePort untuk pertukaran pesan bolak-balik, serta kejujuran modelnya, yaitu tidak ada memori bersama yang dapat diubah karena setiap pesan dikirim sebagai salinan. Semua contoh berjalan penuh di terminal dengan dart:isolate bawaan.

Model isolate tanpa memori bersama#

Setiap program Dart dimulai dengan satu isolate yang menjalankan fungsi main, dan isolate tambahan dibuat seperlunya. Isolate tidak berbagi memori yang dapat diubah, sehingga dua isolate tidak pernah membaca dan menulis objek yang sama secara bersamaan. Komunikasi antarisolate dilakukan lewat pesan, yaitu objek yang disalin ketika dikirim, dengan pengecualian objek tak berubah (immutable) seperti nilai angka dan string yang cukup dirujuk ulang. Tanpa memori bersama, alat sinkronisasi seperti kunci (lock) tidak diperlukan, dan kelas-kelas koleksi tidak perlu dilindungi dari akses serentak. Model ini menempatkan semua risiko pada batas pengiriman pesan, yang justru membuatnya mudah dipahami dan diuji.

Tidak adanya memori bersama juga menyeragamkan cara merancang program paralel. Pekerjaan dibagi menjadi potongan data yang saling lepas, setiap potongan diproses di satu isolate, dan hasilnya dikumpulkan lewat pesan. Kerangka berpikir ini dipertahankan pada seluruh contoh dalam topik ini.

Isolate.run untuk komputasi#

Isolate.run adalah bentuk paling ringkas untuk memindahkan satu perhitungan ke isolate lain. Fungsi yang dikirim dijalankan pada isolate baru, hasil kembaliannya dikirim balik sebagai pesan, dan isolate tersebut ditutup sendiri setelah selesai. Pemanggilannya mengembalikan Future, sehingga hasilnya diambil dengan await dan pemanggil tetap responsif selama perhitungan berlangsung. Contoh berikut menghitung jumlah kuadrat sepuluh angka pertama pada isolate pekerja, yaitu pekerjaan kecil yang sengaja dipilih agar angkanya mudah diperiksa.

dart jumlah-kuadrat.dart
import 'dart:isolate';

Future<void> main() async {
  final angka = List.generate(10, (i) => i + 1);
  final total = await Isolate.run(() => jumlahKuadrat(angka));
  print("jumlah kuadrat 1 sampai 10: $total");
}

int jumlahKuadrat(List<int> daftar) {
  return daftar.fold(0, (jumlah, n) => jumlah + n * n);
}
bash
# menjalankan program dari folder topik
dart run jumlah-kuadrat.dart
text Keluaran
jumlah kuadrat 1 sampai 10: 385

Hasil 385 dapat dihitung ulang dengan mudah, yaitu 1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100. Daftar angka ditangkap oleh penutupan (closure) yang dikirim ke Isolate.run, dan seluruh isinya ikut disalin ke isolate pekerja sesuai model pesan. Untuk komputasi yang lebih besar, beberapa isolate dapat dijalankan sekaligus dan hasilnya dikumpulkan dengan Future.wait yang dibahas pada topik async dan await.

dart dua-potongan.dart
import 'dart:isolate';

Future<int> jumlahPotongan(List<int> potongan) {
  return Isolate.run(() => potongan.reduce((a, b) => a + b));
}

Future<void> main() async {
  final angka = List.generate(100, (i) => i + 1);
  final pertama = jumlahPotongan(angka.sublist(0, 50));
  final kedua = jumlahPotongan(angka.sublist(50));
  final hasil = await Future.wait([pertama, kedua]);
  print("potongan 1: ${hasil[0]}");
  print("potongan 2: ${hasil[1]}");
  print("total 1 sampai 100: ${hasil[0] + hasil[1]}");
}
bash
# menjalankan program dari folder topik
dart run dua-potongan.dart
text Keluaran
potongan 1: 1275
potongan 2: 3775
total 1 sampai 100: 5050

Ratusan angka dipecah menjadi dua potongan, masing-masing dijumlahkan pada isolate sendiri, dan total gabungannya 5050 sesuai rumus penjumlahan 1 sampai 100. Pemanggilan jumlahPotongan hanya memulai pekerjaan, sehingga dua isolate berjalan bersamaan dan Future.wait mengumpulkan hasilnya dalam urutan daftar. Pemisahan pekerjaan menjadi potongan yang saling lepas adalah persiapan penting sebelum menggunakan isolate, karena potongan itulah yang dikirim sebagai pesan.

Isolate.spawn dan pertukaran pesan#

Apabila pekerjaan perlu berkirim pesan lebih dari sekali, Isolate.spawn membuat isolate yang hidup lebih lama dan berkomunikasi lewat port. ReceivePort menerima pesan, sedangkan SendPort mengirim pesan ke port milik isolate lain, dan pasangan ini sering dikirim lebih dahulu agar kedua sisi memiliki jalur balik. Fungsi yang diberikan kepada Isolate.spawn harus berupa fungsi puncak (top-level) atau statis, karena fungsi tersebut dijalankan pada isolate baru tanpa lingkungan pemanggilnya. Diagram berikut merangkum arah pesannya, yaitu salinan yang berpindah sisi, bukan objek yang diakses bersama.

Isolate utamamemori dan event loop sendiriIsolate pekerjamemori dan event loop sendiripesan dikirim (salinan)tidak ada memori bersamabalasan dikirim (salinan)
Pertukaran pesan antara dua isolate melalui port
dart pesan-port.dart
import 'dart:isolate';

Future<void> main() async {
  final portUtama = ReceivePort();
  await Isolate.spawn(hitungKata, portUtama.sendPort);
  final portBalik = await portUtama.first as SendPort;
  final jawaban = ReceivePort();
  portBalik.send(["belajar dart sangat seru", jawaban.sendPort]);
  final jumlah = await jawaban.first;
  print("jumlah kata: $jumlah");
  portUtama.close();
}

void hitungKata(SendPort keUtama) {
  final portKerja = ReceivePort();
  keUtama.send(portKerja.sendPort);
  portKerja.listen((pesan) {
    final teks = pesan[0] as String;
    final balik = pesan[1] as SendPort;
    balik.send(teks.split(" ").length);
  });
}
bash
# menjalankan program dari folder topik
dart run pesan-port.dart
text Keluaran
jumlah kata: 4

Pertukaran pesannya berlangsung dalam tiga langkah dan sesuai arah diagram di atas. Isolate pekerja segera mengirim SendPort miliknya melalui portUtama, sehingga main memperoleh jalur balik pada portUtama.first. Sisi utama kemudian mengirim pesan berupa list yang berisi teks dan SendPort jawaban, pekerja menghitung empat kata pada teks itu, dan hasilnya kembali sebagai pesan yang ditunggu dengan await jawaban.first. Pesan pertama berupa SendPort tetap terkirim dengan baik karena port termasuk objek yang dapat dikirim lintas isolate, sedangkan isi pesannya disalin sebagaimana modelnya.

Pesan dikirim sebagai salinan#

Sifat penyalinan pesan perlu diuji secara langsung agar kejujuran modelnya tidak hanya klaim. Apabila isolate pekerja mengubah objek yang diterimanya, perubahan itu hanya menyentuh salinannya, sedangkan objek asli di sisi utama tetap apa adanya. Perilaku ini berbeda dari model memori bersama karena mutasi pada satu alur langsung terlihat oleh alur lain, dan justru perbedaan itulah yang menghilangkan perlunya kunci sinkronisasi di Dart.

dart salinan.dart
import 'dart:isolate';

Future<void> main() async {
  final nilai = <int>[10, 24, 8];
  final laporan = await Isolate.run(() {
    nilai[0] = 999;
    return "isolate pekerja selesai";
  });
  print(laporan);
  print("list di main: $nilai");
}
bash
# menjalankan program dari folder topik
dart run salinan.dart
text Keluaran
isolate pekerja selesai
list di main: [10, 24, 8]

Isolate pekerja mengisi elemen pertama dengan 999 dan tidak ada penanganan yang melarangnya, karena pengisian dilakukan pada salinan yang sah milik pekerja. Keluaran menunjukkan list di sisi utama tetap [10, 24, 8], sehingga perubahan lintas isolate tidak pernah menembus memori asalnya. Konsekuensi praktisnya sederhana, yaitu hasil pekerjaan lintas isolate selalu dikembalikan sebagai pesan, dan mengandalkan mutasi objek bersama hanya menghasilkan kode yang kelihatannya bekerja padahal tidak pernah memengaruhi sisi penerimanya.

Ringkasan#

  • Isolate adalah unit pekerjaan dengan memori dan event loop sendiri, dan tidak ada memori bersama yang dapat diubah di antara isolate.
  • Isolate.run menjalankan satu perhitungan pada isolate baru dan mengembalikan hasilnya sebagai Future yang diambil dengan await.
  • Isolate.spawn membuat isolate yang hidup lama, dan pesannya bolak-balik melalui pasangan SendPort dan ReceivePort.
  • Setiap pesan disalin ke sisi penerimanya, sehingga mutasi pada salinan tidak pernah memengaruhi objek aslinya.

Referensi#