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:
- Memiliki state privat yang tidak bisa diakses dari luar
- Berkomunikasi hanya melalui message passing
- 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 A
→ DEADLOCK: 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: resume → selesai
→ tidak 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
}