6. Global Actors
6. Global Actors
6.1 Teori: Singleton Isolation Domain
Actor biasa (actor Foo {}) membuat isolation domain per-instance — setiap instance Foo adalah domain yang berbeda. Dua instance tidak berbagi domain:
actor Counter {
var count = 0
}
let c1 = Counter() // isolation domain #1
let c2 = Counter() // isolation domain #2 — terpisah dari #1
Terkadang kita butuh satu domain yang digunakan bersama oleh banyak tipe. Misalnya: semua UIKit calls harus di satu thread (main thread). Kita tidak ingin setiap ViewController punya actor-nya sendiri — kita ingin semua berbagi satu domain: "main thread".
Global Actor adalah actor singleton yang bisa digunakan sebagai atribut (@MainActor, @DatabaseActor, dll) untuk menyatakan bahwa sebuah tipe atau method berbagi domain tersebut.
6.2 @MainActor — Global Actor Paling Penting
@MainActor mewakili main thread. Ini adalah global actor bawaan Swift karena kebutuhan ini sangat universal dalam pengembangan aplikasi.
Mengapa semua UI harus di main thread?
- UIKit dan AppKit tidak thread-safe — mereka tidak dirancang untuk concurrent access
- Main thread adalah satu-satunya thread yang berkomunikasi dengan window server (macOS/iOS)
- Animasi, layout, dan rendering semua dikoordinasikan melalui run loop di main thread
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
@Published var isLoading: Bool = false
// Semua property dan method di sini dijamin di main thread
func loadData() async {
isLoading = true // OK: @MainActor, pasti di main thread
// fetchFromNetwork() tidak @MainActor → berjalan di background
let result = await fetchFromNetwork()
// Setelah await, kita kembali ke @MainActor context
title = result // OK: kembali di main thread
isLoading = false
}
private func fetchFromNetwork() async -> String {
// Tidak ada @MainActor annotation → background thread
try? await Task.sleep(for: .seconds(1))
return "Data loaded"
}
}
Kapan gunakan @MainActor:
ViewControllerdan subclassnya — selaluViewModelyang mengelola@Publishedproperties — selalu- Delegate methods yang update UI
- Callback dari timer/notification yang perlu update UI
Kapan TIDAK gunakan @MainActor:
- Business logic yang tidak menyentuh UI — ini akan memblokir/mengganggu responsivitas UI
- Network layer, database layer, parsing logic — semua ini lebih baik di background actor
- Fungsi komputasi berat — bisa menyebabkan UI freeze
6.3 Custom Global Actor
// Definisi global actor — satu per domain yang diinginkan
@globalActor
actor NetworkActor {
static let shared = NetworkActor() // WAJIB: singleton
}
// Semua tipe yang dianotasi @NetworkActor berbagi isolation domain yang sama
@NetworkActor
class NetworkManager {
var activeRequests: [UUID: URLSessionTask] = [:]
func send(_ request: URLRequest) async throws -> Data {
let task = URLSession.shared.dataTask(with: request)
return Data()
}
}
@NetworkActor
class RequestInterceptor {
// Ini berbagi domain dengan NetworkManager
// Akses ke NetworkManager dari sini tidak butuh await!
}
@NetworkActor
func cancelAllRequests(manager: NetworkManager) {
// Berada di NetworkActor domain — bisa akses manager.activeRequests langsung
manager.activeRequests.values.forEach { $0.cancel() }
manager.activeRequests.removeAll()
}
6.4 Global Actor vs Regular Actor: Kapan Memilih Mana?
Gunakan Global Actor (@DatabaseActor dll) jika:
┌──────────────────────────────────────────────────────┐
│ Banyak class/struct berbagi isolation domain yang sama│
│ Kamu ingin anotasi atribut (@DatabaseActor) │
│ Domain punya nama konseptual yang jelas │
│ Contoh: semua database access, semua network calls │
└──────────────────────────────────────────────────────┘
Gunakan Regular Actor jika:
┌──────────────────────────────────────────────────────┐
│ Satu instance dengan state yang perlu dilindungi │
│ Tidak ada kebutuhan berbagi domain dengan tipe lain │
│ Contoh: cache instance, counter, data store │
└──────────────────────────────────────────────────────┘
Bahaya Global Actor: Jika terlalu banyak tipe diisolasi ke satu global actor, kamu menciptakan bottleneck — semua operasi yang diisolasi harus antri di satu executor. Untuk throughput tinggi, lebih baik gunakan banyak actor instance terpisah.
6.5 Global Actor pada Partial Class/Struct
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
// Hanya method tertentu yang diisolasi ke DatabaseActor
class DataRepository {
@DatabaseActor
func fetchUser(id: String) async -> User? {
// berjalan di DatabaseActor
return nil
}
@DatabaseActor
var cachedUsers: [User] = []
// Method ini berjalan di caller's context — tidak diisolasi
func prepareQuery(id: String) -> String {
return "SELECT * WHERE id = '\(id)'"
}
}