Section 2/133 menit

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:

  1. Setiap "actor" adalah entitas independen dengan state privatnya sendiri
  2. Actors berkomunikasi hanya melalui pesan — tidak ada shared state
  3. 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.

swift
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 antrianvalue += 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?

swift
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.

swift
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:

swift
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.

swift
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:

swift
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))
    }
}