Section 8/161 menit
8. Actor Re-entrancy: Patterns dan Pitfalls
8. Actor Re-entrancy: Patterns dan Pitfalls
Re-entrancy adalah properti fundamental actor di Swift: saat actor await sesuatu, actor bisa melayani request lain sebelum await selesai.
Visualisasi Re-entrancy
swift
Actor UserCache menjalankan dua task konkuren:
Task A: fetchUser("alice") Task B: fetchUser("alice")
│ │
├─ guard !cache["alice"] │
│ (cache kosong, lanjut) │
│ ├─ guard !cache["alice"]
├─ await network.fetch("alice")◄──suspend (actor bisa layani task lain)
│ │ (cache masih kosong!)
│ ◄──┤─ await network.fetch("alice")
│ │ suspend
│◄── resume: alice data tiba │
├─ cache["alice"] = alice │
├─ return alice │
◄──┤─ resume: alice data tiba (DUPLIKAT!)
├─ cache["alice"] = alice (redundant)
├─ return alice
Pola Aman: Check-Then-Act Tanpa Suspension di Antara
swift
actor UserCache {
private var cache: [String: User] = [:]
private var inFlight: [String: Task<User, Error>] = [:]
// ❌ Pattern berbahaya: check dan set dipisah oleh await
func fetchUserUnsafe(id: String) async throws -> User {
if let cached = cache[id] { return cached }
let user = try await NetworkClient.fetchUser(id: id) // Suspension!
// State bisa berubah di sini — task lain bisa sudah isi cache[id]
cache[id] = user // Mungkin overwrite pekerjaan task lain
return user
}
// ✓ Pattern aman: deduplication dengan task tracking
func fetchUser(id: String) async throws -> User {
// Check cache — tidak ada suspension
if let cached = cache[id] { return cached }
// Check apakah sudah ada request yang in-flight
if let existingTask = inFlight[id] {
return try await existingTask.value // Tunggu task yang sudah ada
}
// Buat task baru dan daftarkan ke inFlight SEBELUM await
let task = Task<User, Error> {
try await NetworkClient.fetchUser(id: id)
}
inFlight[id] = task
do {
let user = try await task.value // Suspension di sini
// Setelah resume: update cache
cache[id] = user
inFlight.removeValue(forKey: id)
return user
} catch {
inFlight.removeValue(forKey: id)
throw error
}
}
}
Pola Aman: Snapshot Sebelum Suspension
swift
actor OrderProcessor {
var pendingOrders: [Order] = []
var processedCount: Int = 0
// ❌ Rentan re-entrancy
func processAllUnsafe() async {
for order in pendingOrders { // Iterasi array yang bisa berubah!
await process(order) // Suspension: pendingOrders bisa berubah
}
}
// ✓ Snapshot dulu, baru iterasi
func processAll() async {
let snapshot = pendingOrders // Copy — tidak terpengaruh mutation lain
pendingOrders = [] // Clear list sebelum suspension
processedCount += snapshot.count // Update sebelum suspension
for order in snapshot {
await process(order) // Suspension aman — snapshot tidak berubah
}
}
}
Pola Aman: State Machine Eksplisit
swift
actor FileDownloader {
enum State {
case idle
case downloading(URL)
case processing(URL, Data)
case done(URL, Data)
case failed(URL, Error)
}
private var state: State = .idle
func download(from url: URL) async throws -> Data {
// State machine mencegah operasi konkuren yang bertentangan
guard case .idle = state else {
throw DownloadError.alreadyInProgress
}
state = .downloading(url) // Set SEBELUM await
do {
let data = try await URLSession.shared.data(from: url).0
state = .processing(url, data)
let processed = try await processData(data)
state = .done(url, processed)
return processed
} catch {
state = .failed(url, error)
throw error
}
}
var isIdle: Bool {
if case .idle = state { return true }
return false
}
}