2. Actor dan Isolation Dasar
2. Actor dan Isolation Dasar
2.1 Sejarah dan Teori: Actor Model
Actor model bukan konsep baru — ia dicetuskan oleh Carl Hewitt pada tahun 1973 sebagai model komputasi untuk sistem concurrent. Prinsipnya sederhana:
- Setiap "actor" adalah entitas independen dengan state privatnya sendiri
- Actors berkomunikasi hanya melalui pesan — tidak ada shared state
- Setiap actor memproses satu pesan pada satu waktu (secara serial)
Bahasa seperti Erlang dan Elixir membangun seluruh sistem di atas actor model — itulah mengapa sistem telecom yang menggunakan Erlang bisa mencapai "nine nines" availability (99.9999999% uptime).
Swift mengadopsi versi yang lebih pragmatis dari actor model: Swift actor tetap bisa memanggil fungsi async eksternal (sehingga bisa menunggu I/O tanpa memblokir), tapi state internalnya tetap serial dan terlindungi.
2.2 Cara Kerja Actor di Bawah Hood
Setiap actor di Swift memiliki sebuah executor — bayangkan ini sebagai "message queue" yang sangat efisien. Ketika kamu memanggil method actor dari luar, Swift tidak langsung mengeksekusinya — ia mengirim "pesan" (function call) ke queue actor tersebut.
Thread A: await counter.increment()
│
└─── Swift runtime: tambahkan 'increment' ke antrian Counter
Thread A suspend (tidak block!)
Thread B: ... mengerjakan hal lain ...
Counter executor: proses 'increment' dari antrian → value += 1
kirim hasil kembali ke Thread A
Thread A: resume, lanjutkan eksekusi
Ini berbeda dari mutex/lock:
- Lock: Thread A memblokir (busy-wait atau OS-wait) sambil menunggu lock bebas
- Actor: Thread A di-suspend (kooperatif) — thread tersebut bebas mengerjakan Task lain selagi menunggu
Ini adalah cooperative concurrency, bukan preemptive locking. Jauh lebih efisien.
2.3 actor vs class: Kapan Memilih Mana?
Gunakan `actor` jika: Gunakan `class` jika:
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ Punya mutable state │ │ Selalu @MainActor (UI layer) │
│ Diakses dari concurrent contexts │ │ Perlu inheritance (subclassing) │
│ Butuh thread-safety otomatis │ │ Dipanggil hanya dari satu thread │
│ Contoh: cache, data store, │ │ Contoh: ViewController, │
│ network layer, counter │ │ ViewModel (@MainActor) │
└──────────────────────────────────┘ └──────────────────────────────────┘
Kapan TIDAK menggunakan actor:
- Ketika datamu adalah value type (struct/enum) — tidak perlu isolation karena copy semantics
- Ketika semua akses sudah terjamin di satu thread (misalnya @MainActor)
- Ketika performance sangat kritis dan overhead actor hop tidak dapat diterima (misalnya tight loop numerik)
- Ketika kamu butuh inheritance — actor tidak bisa di-subclass
2.4 Konsep
actor adalah reference type seperti class, tapi semua stored property-nya diisolasi secara otomatis — hanya bisa diakses dari dalam actor itu sendiri tanpa await.
actor Counter {
private(set) var value = 0
func increment() {
value += 1 // OK: berada di dalam isolation domain actor ini
}
func reset() {
value = 0 // OK: sama
}
}
2.5 Mengakses dari Luar Actor
Akses dari luar harus menggunakan await karena melewati isolation boundary — ini adalah "hop" ke executor actor:
let counter = Counter()
Task {
await counter.increment() // hop ke Counter executor, lalu kembali
let v = await counter.value // hop lagi untuk baca property
print(v)
}
Setiap await adalah titik suspension potensial. Thread kamu dibebaskan untuk mengerjakan hal lain selagi menunggu actor.
2.6 Actor Reentrancy — Konsep Penting yang Sering Disalahpahami
Actor bersifat reentrant — ini keputusan desain yang disengaja, bukan bug.
Mengapa reentrant? Bayangkan actor A menunggu actor B, dan actor B menunggu actor A — jika actor tidak reentrant, ini adalah deadlock permanen. Reentrancy mencegah deadlock dengan memperbolehkan actor menerima pesan lain selagi menunggu await.
Trade-off: State bisa berubah di titik suspension.
actor BankAccount {
var balance: Double = 1000
func withdraw(amount: Double) async {
guard balance >= amount else { return }
// ⚠️ TITIK SUSPENSION — actor bisa terima pesan lain di sini
await processPayment(amount: amount)
// balance MUNGKIN sudah berubah saat kita kembali!
// Jika ada dua concurrent withdraw(500), keduanya bisa lolos guard di atas
// karena keduanya membaca balance = 1000 sebelum suspension
balance -= amount // bisa jadi negatif!
}
}
Aturan: Selalu periksa ulang asumsi setelah setiap await, atau lakukan mutasi state sebelum await:
actor BankAccount {
var balance: Double = 1000
func withdraw(amount: Double) async -> Bool {
// Kurangi balance SEBELUM await — tidak ada window untuk race
guard balance >= amount else { return false }
balance -= amount // atomic dalam konteks actor
// Bahkan jika ada concurrent withdraw lain masuk sini,
// balance sudah dikurangi — mereka akan gagal di guard di atas
await processPayment(amount: amount)
return true
}
private func processPayment(amount: Double) async {
try? await Task.sleep(for: .seconds(1))
}
}