Section 3/202 menit

3. Actors

3. Actors

Teori: Model Actor dan Serial Execution

Actor didasarkan pada Actor Model yang dikembangkan Carl Hewitt (1973) — model komputasi di mana "actors" adalah unit dasar yang:

  1. Memiliki state privat yang tidak bisa diakses dari luar
  2. Berkomunikasi hanya melalui message passing
  3. Memproses satu message pada satu waktu (serial)

Swift actors mengimplementasikan ini dengan executor — sebuah serializer yang memastikan hanya satu piece of code yang mengakses actor state pada satu waktu. Ini berbeda dari mutex/lock:

swift
Lock/Mutex:                      Actor:
──────────────────               ────────────────────
Thread A: lock()                 Task A: await actor.method()
Thread A: critical section       Task A: suspended (tapi TIDAK block thread)
Thread B: lock() → BLOCKED       Thread: free, bisa kerjakan Task B
  (thread terbuang menunggu)     Task A: resumed saat actor tersedia
Thread A: unlock()               Task A: runs in actor's executor
Thread B: runs                   (tidak ada thread yang terbuang)

Keunggulan actor vs lock:

  • Tidak ada thread blocking → lebih efisien
  • Tidak ada deadlock dari nested locks
  • Compiler enforcement → tidak bisa "lupa" lock
  • Lebih composable

Cara Kerja Internal

swift
actor Counter {
    var value = 0
    func increment() { value += 1 }
}

Di balik layar, compiler men-generate sesuatu setara dengan:

swift
// Konseptual — bukan kode aktual Swift
class Counter {
    private var value = 0
    private let executor = SerialExecutor()  // antrian serial

    func increment() {
        executor.enqueue {
            self.value += 1  // hanya berjalan dalam executor
        }
    }
}

Reentrancy — Desain yang Disengaja

Actor bersifat reentrant: ketika actor sedang menunggu await di dalam method-nya, actor bisa menerima dan memproses request lain. Ini bukan bug — ini desain untuk mencegah deadlock:

swift
Tanpa reentrancy (deadlock risk):
Actor A menunggu Actor B
Actor B menunggu Actor ADEADLOCK: keduanya tunggu selamanya

Dengan reentrancy:
Actor A menunggu Actor B (via await)
Actor A: "saya suspend, silakan proses request lain"
Actor B: selesai, kirim hasil ke Actor A
Actor A: resumeselesaitidak ada deadlock

Implikasi praktis reentrancy:

swift
actor Inventory {
    var stock = 10

    func purchaseItem() async -> Bool {
        guard stock > 0 else { return false }  // check: stock = 10 ✓

        await processPayment()  // SUSPEND — actor bisa terima request lain!
        // Saat resume: stock mungkin sudah berubah!

        stock -= 1  // mungkin stock sudah 0 dari request lain → negative!
        return true
    }
}

// FIX: modifikasi state SEBELUM suspension point
actor Inventory {
    var stock = 10

    func purchaseItem() async -> Bool {
        guard stock > 0 else { return false }
        stock -= 1  // kurangi SEBELUM await → aman dari reentrancy

        await processPayment()  // suspend boleh setelah state di-update
        return true
    }
}

Kapan Menggunakan Actor

Gunakan actor ketika:

  • Ada shared mutable state yang diakses dari multiple concurrent contexts
  • State terdiri dari beberapa field yang harus konsisten satu sama lain
  • Operasi bersifat stateful (counter, cache, queue)
  • Perlu koordinasi antar-tasks

Jangan gunakan actor ketika:

  • State adalah value type (struct/enum) — tidak perlu protection
  • Semua akses sudah di satu thread (gunakan @MainActor saja)
  • Butuh inheritance — actor tidak bisa di-subclass
  • Operasi sangat simple dan lightweight — overhead actor hop mungkin signifikan

Real Use Case: Session Manager

swift
// Session management yang thread-safe tanpa lock manual
actor SessionManager {
    private var sessions: [String: UserSession] = [:]
    private var cleanupTask: Task<Void, Never>?
    private let sessionTimeout: Duration

    init(sessionTimeout: Duration = .seconds(3600)) {
        self.sessionTimeout = sessionTimeout
    }

    func createSession(for userID: String) -> UserSession {
        let session = UserSession(userID: userID, expiresAt: .now + sessionTimeout)
        sessions[session.id] = session
        startCleanupIfNeeded()
        return session
    }

    func validate(sessionID: String) -> UserSession? {
        guard let session = sessions[sessionID] else { return nil }
        if session.expiresAt < .now {
            sessions.removeValue(forKey: sessionID)
            return nil
        }
        return session
    }

    func invalidate(sessionID: String) {
        sessions.removeValue(forKey: sessionID)
    }

    func invalidateAll(for userID: String) {
        sessions = sessions.filter { $0.value.userID != userID }
    }

    private func startCleanupIfNeeded() {
        guard cleanupTask == nil else { return }
        cleanupTask = Task { [weak self] in
            while !Task.isCancelled {
                try? await Task.sleep(for: .seconds(300))
                await self?.cleanExpiredSessions()
            }
        }
    }

    private func cleanExpiredSessions() {
        let now = ContinuousClock.now
        sessions = sessions.filter { $0.value.expiresAt >= now }
    }
}

struct UserSession: Sendable {
    let id: String = UUID().uuidString
    let userID: String
    var expiresAt: ContinuousClock.Instant
}