This content originally appeared on DEV Community and was authored by ArshTechPro
How Actors Work
An actor processes one request at a time in a serialized manner. When you call an actor’s method from outside its isolation context, you must use await
, which creates a suspension point where your code waits for its turn to execute.
actor DownloadManager {
private var activeDownloads: [URL: Progress] = [:]
private var completedFiles: [URL: Data] = [:]
func startDownload(from url: URL) -> String {
if activeDownloads[url] != nil {
return "Download already in progress"
}
let progress = Progress()
activeDownloads[url] = progress
return "Download started"
}
func completeDownload(url: URL, data: Data) {
activeDownloads.removeValue(forKey: url)
completedFiles[url] = data
}
func getCompletedData(for url: URL) -> Data? {
return completedFiles[url]
}
}
// Usage requires await
func handleDownload() async {
let manager = DownloadManager()
let status = await manager.startDownload(from: URL(string: "https://example.com/file")!)
print(status)
}
For details about MainActor refer below article
https://dev.to/arshtechpro/understanding-mainactor-in-swift-when-and-how-to-use-it-4ii4
What are Global Actors?
Global actors are a powerful Swift concurrency feature that extend the actor model to provide app-wide synchronization domains. While regular actors protect individual instances, global actors ensure that multiple pieces of code across different types and modules execute on the same serialized executor.
Think of a global actor as a synchronization coordinator that manages access to shared resources across your entire application. The most familiar example is @MainActor
, which ensures code runs on the main thread.
Why Do We Need Global Actors?
Global actors solve specific synchronization challenges:
- Cross-Type Coordination: When multiple classes need to work with the same shared resource
- Thread Affinity: Ensuring certain code always runs on specific threads (like UI on main thread)
- Domain Isolation: Keeping different subsystems (networking, database, analytics) properly synchronized
- Compile-Time Safety: Moving thread-safety from runtime checks to compile-time guarantees
Creating a Global Actor
To create a global actor, you need:
- The
@globalActor
attribute - A shared static instance
- The actor keyword
Here’s the basic structure:
@globalActor
actor MyCustomActor {
static let shared = MyCustomActor()
private init() {}
}
How to Use Global Actors
Global actors can be applied at three levels:
1. Class-Level Application
When you mark an entire class with a global actor, all its properties and methods become part of that actor’s domain:
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
private init() {}
}
@DatabaseActor
class DatabaseManager {
private var cache: [String: Any] = [:]
private var transactionCount = 0
func save(key: String, value: Any) {
cache[key] = value
transactionCount += 1
print("Saved: \(key) - Total transactions: \(transactionCount)")
}
func retrieve(key: String) -> Any? {
return cache[key]
}
func clearCache() {
cache.removeAll()
print("Cache cleared")
}
}
// Usage
class ViewController: UIViewController {
let database = DatabaseManager()
func saveUserData() async {
// Must use await - accessing DatabaseActor from outside
await database.save(key: "username", value: "John")
await database.save(key: "lastLogin", value: Date())
// All these calls are synchronized - no race conditions
if let username = await database.retrieve(key: "username") {
print("Retrieved: \(username)")
}
}
}
2. Method-Level Application
You can mark specific methods to run on a global actor while keeping the rest of the class unaffected:
class DataService {
private var localCache: [String: String] = []
// Regular method - runs on any thread
func processData(_ input: String) -> String {
return input.uppercased()
}
// This method runs on DatabaseActor
@DatabaseActor
func syncToDatabase(_ data: String) {
print("Syncing to database: \(data)")
// This is synchronized with all other DatabaseActor code
}
// This method runs on MainActor
@MainActor
func updateUI(with message: String) {
// Safe to update UI here
NotificationCenter.default.post(
name: .dataUpdated,
object: message
)
}
func performCompleteSync() async {
let processed = processData("hello world")
await syncToDatabase(processed)
await updateUI(with: "Sync complete")
}
}
3. Property-Level Application
Individual properties can be bound to global actors:
class AppSettings {
@DatabaseActor var userData: [String: Any] = [:] // Bound to DatabaseActor
@MainActor var currentTheme: String = "light" // Bound to MainActor
var cacheSize: Int = 100 // Not actor-bound
func updateSettings() async {
// Need await for DatabaseActor property
await DatabaseActor.run {
userData["lastUpdate"] = Date()
}
// Need await for MainActor property
await MainActor.run {
currentTheme = "dark"
}
// No await needed for regular property
cacheSize = 200
}
}
Running Code on Global Actors
You can explicitly run code on a global actor using the run
method:
// Run a closure on MainActor
await MainActor.run {
// Update UI safely
myLabel.text = "Updated"
}
// Run on your custom actor
await DatabaseActor.run {
// This code runs on DatabaseActor
print("Running database operation")
}
Nonisolated and Global Actors
You can opt specific members out of global actor isolation:
@DatabaseActor
class DataStore {
private var records: [String: Any] = [:]
let storeId = UUID() // Immutable - safe to access
// This property doesn't need synchronization
nonisolated var debugDescription: String {
return "DataStore: \(storeId)"
}
// This method can be called without await
nonisolated func validateKey(_ key: String) -> Bool {
return !key.isEmpty && key.count < 100
}
// This needs synchronization - accesses mutable state
func addRecord(key: String, value: Any) {
records[key] = value
}
}
// Usage
let store = DataStore()
print(store.debugDescription) // No await needed
let isValid = store.validateKey("myKey") // No await needed
await store.addRecord(key: "myKey", value: "data") // Await required
Best Practices
Use Meaningful Names: Name your global actors based on their purpose (DatabaseActor, NetworkActor, etc.)
Keep Global Actors Focused: Each global actor should have a single, clear responsibility
Minimize Cross-Actor Communication: Frequent switching between actors impacts performance
Use @MainActor for UI: Always use @MainActor for UI updates rather than creating custom UI actors
Consider Performance: Global actors serialize access – only use when synchronization is needed
Common Pitfalls to Avoid
Over-using Global Actors: Don’t mark everything with a global actor – only use when you need synchronization
Blocking Operations: Avoid long-running synchronous operations in global actors as they block other operations
Circular Dependencies: Be careful when global actors call each other – this can lead to deadlocks
Summary
Global actors are a powerful tool for managing synchronization across your entire application. By understanding global actors, you can write safer concurrent code.
This content originally appeared on DEV Community and was authored by ArshTechPro