Section 7/161 menit

7. Actor Reentrancy dan Isolation Deep Dive

7. Actor Reentrancy dan Isolation Deep Dive

Memahami Reentrancy

Actor di Swift bersifat reentrant: ketika actor menunggu di await, ia bisa melayani request lain. Ini berbeda dari mutex yang memblokir.

swift
// Visualisasi reentrancy
actor BankAccount {
    var balance: Double = 1000.0

    func transfer(amount: Double, to target: BankAccount) async throws {
        guard balance >= amount else { throw BankError.insufficient }

        balance -= amount  // (A) balance = 900

        // ⚠️ Suspension point: actor bisa layani request lain di sini!
        try await target.deposit(amount)  // (B) bisa ada call lain ke self masuk

        // Jika ada call lain yang ubah balance antara (A) dan (C)
        // state bisa tidak konsistent
    }  // (C)

    func deposit(_ amount: Double) async {
        balance += amount
    }
}

// Contoh race yang mungkin terjadi:
// 1. transfer(500) dimulai: balance = 500
// 2. Saat await, transfer(600) masuk: balance = 500 - 600 = -100 (!!)
// 3. transfer(500) resume: deposit selesai
// → balance negatif meski ada guard di awal
swift
// Fix: snapshot state sebelum suspension, validasi setelah resume
actor BankAccount {
    var balance: Double = 1000.0

    func transfer(amount: Double, to target: BankAccount) async throws {
        // Validasi dan kurangi balance SEBELUM suspension
        guard balance >= amount else { throw BankError.insufficient }
        balance -= amount

        do {
            try await target.deposit(amount)
        } catch {
            // Rollback jika deposit gagal
            balance += amount
            throw error
        }
        // Tidak ada state yang perlu di-revalidasi setelah resume
    }

    func deposit(_ amount: Double) async {
        balance += amount
    }
}

enum BankError: Error { case insufficient }

nonisolated dan Sendable

swift
// nonisolated: method yang tidak butuh actor isolation
actor UserCache {
    private var cache: [String: User] = [:]

    // ✓ nonisolated: tidak akses mutable state
    nonisolated var description: String {
        "UserCache instance"
    }

    // ✓ nonisolated computed property aman jika hanya akses let
    nonisolated let cacheVersion: Int = 1

    // ⚠️ Tidak bisa nonisolated jika akses var
    func updateUser(_ user: User) {
        cache[user.id] = user
    }
}

// Sendable: syarat untuk crossing actor boundary
struct User: Sendable {
    let id: String
    let name: String
    // Semua stored properties harus Sendable
}

// Closure yang crossing actor boundary harus @Sendable
func scheduleWork(actor: some Actor, work: @escaping @Sendable () async -> Void) {
    Task {
        await work()
    }
}

Isolated Parameters (Swift 5.7+)

swift
// isolated parameter: specify bahwa fungsi berjalan di isolasi aktor tertentu
func updateUI(isolated actor: MainActor.Type = MainActor.self) {
    // Dipanggil di main actor context
    label.text = "Updated"
}

// Lebih berguna dengan actor instance
func process(isolated account: BankAccount) async {
    // Berjalan di isolasi account actor
    // Bisa akses internal state account secara langsung
    account.balance += 100  // aman karena isolated
}

// Gunakan untuk fungsi yang perlu akses ke actor tanpa jadi method
extension BankAccount {
    static func merge(
        _ source: isolated BankAccount,
        into destination: isolated BankAccount
    ) async {
        // Harus berjalan di kedua actor — tidak mungkin secara simultan
        // Swift memanggil ini dua kali secara bergantian
        let amount = source.balance
        source.balance = 0
        destination.balance += amount
    }
}

var label: UILabel { fatalError() }