Today we are going to explore Swift Actors vs GlobalActors vs MainActor, as per normally I will explain to you deeply how the things became things. Swift 5.5 introduced. Actors to enhance concurrency by preventing data races and ensuring safe access to shared mutable state. Here in this article you will be learning about ActorsGlobalActorsMainActor, and their entire ecosystem deeply. We’ll also observe how the Swift compiler, LLVM, and Swift concurrency model internally handle them. 
Before I even got into writing this article, a bunch of questions were still up in the air, and they really piqued my interest.

  • What Actors are and how they work internally?
  • How Actors ensures isolation and prevents data races?
  • What is the role of GlobalActors and MainActor?
  • How the Swift compiler manages actor-based concurrency?
  • Best practices, pitfalls, and performance considerations

1. What Actors are and how they work internally?

An Actor in Swift is a reference type (like a class) that ensures thread-safe access to its mutable state by serializing tasks through an internal executor (queue). It prevents race conditions by allowing only one task to modify its state at a time. Unlike locks, Through structured concurrency, they manage execution safely
without manual synchronization., requiring await for safe access. When an async function suspends, the actor becomes reentrant, allowing other tasks to execute in the meantime. Internally, Swift’s runtime schedules tasks efficiently, ensuring safe and optimized concurrency without manual synchronization.

1.1 Actor Syntax & Basics

actor BankManager {
    var balance: Double = 0.0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func getBalance() -> Double {
        return balance
    }
}

Key Properties of Actors
✔️ Isolation: Only one task can access the mutable state at a time.
✔️ Concurrency-safe: Prevents data races without requiring locks.
✔️ Reference Type: Like a class, but it enforces isolation.

1.2 Calling Actor Methods

Because actors are isolated, calling methods from outside requires awaiting an asynchronous execution:

let manager = BankManager()

Task {
    await manager.deposit(amount: 100.0)
    print(await manager.getBalance()) // 100.0
}

💡 Key takeaway:

  • Direct actor method calls require await because they execute asynchronously.
  • Immutable properties can be accessed synchronously:
actor Counter {
    let name: String = "MyCounter"
}

let counter = Counter()
print(counter.name) // ✅ Allowed, because `name` is immutable.

2. How Actors ensures isolation and prevents data races?

Actors guarantee isolation and prevent data races by enforcing exclusive access to their mutable state. Internally, each actor has a dedicated executor (queue) that serializes tasks, allowing only one operation to modify its properties at a time. Direct access to an actor’s state from outside is prohibited, requiring await for controlled interaction. This prevents multiple threads from reading and writing simultaneously, eliminating race conditions. Furthermore, Swift’s actor reentrancy allows other tasks to execute while an actor is waiting on an await operation, ensuring efficiency without compromising safety.

2.1 Unsafe code:

class UnsafeCounter {
    var value = 0
}

let counter = UnsafeCounter()

DispatchQueue.concurrentPerform(iterations: 10) {
    counter.value += 1 // 🚨 Data race
}

💥 The above code is unsafe because multiple threads can access value simultaneously

2.2 Actor ensuring safety

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

let counter = SafeCounter()

Task {
    await counter.increment() // ✅ Safe
}

🔹 Why is this safe?
Because Swift guarantees that only one task at a time can access value.


3. What is the role of GlobalActors and MainActor?

In Swift, GlobalActor and @MainActor are used to manage concurrency by ensuring that specific pieces of code execute on a designated actor, helping to prevent data races.

3.1 What Are GlobalActor?

Swift’s GlobalActor is a special type of actor that synchronizes access across multiple instances. define a singleton actor that provides execution context for specific functions, properties, or types. You can use it to group related code that should always execute on the same actor.

@globalActor
struct MyGlobalActor {
    static let shared = MyActor()
}

actor MyActor {
    func doWork() {
        print("Executing on MyActor")
    }
}

@MyGlobalActor
func someFunction() {
    print("This function runs on MyGlobalActor")
}

MyGlobalActor is a custom global actor. Any function, property, or type annotated with @MyGlobalActor will execute within MyActor but there is a limitation by design but not necessarily a flaw. global actor must have exactly one shared actor instance. You cannot have multiple shared instances inside a @globalActor. The compiler will reject this because it won’t know which actor to use.

3.2 What Are MainActor?

is a built-in global actor that ensures the annotated code runs on the main thread. It is crucial for updating UI in SwiftUI and UIKit applications.
You can use it when you want to handling view-related logic or ensuring that critical code executes on the main thread.

@MainActor
class ViewModel {
    var text: String = "Loading..."
    
    func updateText() {
        text = "Updated!"
    }
}

Equivalent to:

class ViewModel {
    @MainActor var text: String = "Loading..."
    
    @MainActor func updateText() {
        text = "Updated!"
    }
}

Key differences between GlobalActor and @MainActor

FeatureGlobalActor@MainActor
PurposeDefines a custom global execution context.Ensures execution on the main thread.
CustomizationYes, you can create multiple global actors.No, it is predefined.
Use CaseEnforcing execution order for shared resources.UI updates and main-thread-only operations.

4. How the Swift compiler manages actor-based concurrency?

4.1 The Role of swiftc (Swift Compiler) in Actor Isolation

The Swift compiler swiftc enforces actor-based concurrency by ensuring that all access to actor-isolated state is properly synchronized. It does this through actor isolation rules, which prevent data races and enforce structured concurrency. When you mark a type with actor, the compiler restricts direct access to its mutable state from outside. The compiler also detects nonisolated functions, allowing them to run outside the actor’s execution context when needed. Through static analysisswiftc guarantees that only one execution context mutates its state at a time, making concurrency safer and more predictable.

When you write:

actor MyActor {
    var count = 0
    func increment() {
        count += 1
    }
}

The Swift compiler swiftc transform it into something like that:

class MyActor : public swift::Actor {
private:
    int count;
public:
    void increment() {
        // Implicitly async-safe
        count++;
    }
}

Here’s what happens step by step:

  1. Actor Isolation Analysis: The compiler ensures that all accesses to mutable actor properties go through async barriers.
  2. Sendable Checking: The compiler enforces that actor states comply with Sendable for safe cross-thread access.
  3. Message Passing Model: Under the hood, actors use a run queue model similar to an event loop.

4.2 Actor’s Execution Model in LLVM

The basis for such is Swift actors use a highly optimized execution model built on LLVM to manage concurrency efficiently. Instead of using the traditionally lock / unlock mechanisms, Swift actors use Message-passing model, similar to Erlang or channels in Go, which ensure that only one task runs at any given time within any given actor.
Every actor has a Job Queue, we calling that a FIFO (First-In, First-Out) queue of tasks that are executed singly in order, just because of that we saying actors prevents data races.
When an async function within an actor calls await, Swift use continuations, a bloc of logic based on LLVM coroutine mechanism to suspend a task without blocking the thread. When the awaited operation completes, the task resumes exactly where it paused, and this is the secret of the power and the efficient of swift actors.

  • Job Queues: A FIFO queue that executes tasks one by one.
  • Continuations: Swift use LLVM-based coroutines (async/await) to suspend and resume tasks.
  • Actor Locks, No! Unlike traditional locks, Swift actors use message passing

5. Best Practices & Performance Considerations

Swift actors help to manage concurrency safety, but they are not always the best choice. Understanding when to use them and how they impact performance is very important if you want to writing efficient code.

5.1 When to Use Actors?

Actors are useful when multiple parts of your app need to access and modify the same data safely. as I explained they prevent race conditions by ensuring that only one task modifies the data at a time. This is useful for shared mutable state, caches, and replacing complex locking mechanisms.

ScenarioWhy?
Protecting shared mutable statePrevents race conditions
Managing concurrent data (caches)Ensures consistency and avoids corruption
Avoiding locks (DispatchQueue)Safer and easier than manual synchronization
✅ When to use actors

5.2 When are Actors unnecessary?

Actors add overhead, so they are unnecessary when performance is critical or when data is already immutable. If a struct can do the job without needing synchronization, it is the better choice.

ScenarioBetter Alternative
Performance-critical operationsDirect access (no actor)
Data never changes (immutable)Use a struct
No need for global coordinationUse local variables
❌ When are Actors unnecessary

5.3 Performance Considerations

Also actors work by message-passing, meaning every task must wait its turn in a queue. Frequent cross-actor calls slow things down. A better approach is to store frequently accessed data inside the actor to reduce unnecessary communication.

5.3.1 Bad Approach (Inefficient Cross-Actor Calls)

actor InvoiceGenerator {
    func generateInvoice(for userId: String) async -> Invoice {
        // ❌ Cross-actor call 1
        let userDetails = await fetchUserDetails(userId: userId)
        
        // ❌ Cross-actor call 2
        let purchaseHistory = await fetchPurchaseHistory(userId: userId) 
        
        return Invoice(user: userDetails, purchases: purchaseHistory)
    }
}

func processInvoices(userIds: [String]) async {
    let generator = InvoiceGenerator()
    
    for userId in userIds {
        // ❌ Multiple calls to actor
        let invoice = await generator.generateInvoice(for: userId)
        
        print("Generated invoice for \(invoice.user.name)")
    }
}

5.3.2 Better Approach (Batching Requests Inside the Actor)

actor InvoiceGenerator {
    func generateInvoices(for userIds: [String]) async -> [Invoice] {
        // ✅ Batch fetch
        let userDetails = await getMultipleUserDetails(userIds: userIds)
        
        // ✅ Batch fetch
        let purchasesList = await getMultiplePurchases(userIds: userIds)
        
        return userIds.compactMap { id in
            guard let user = userDetails[id],
                  let purchases = purchasesList[id] 
            else { return nil }

            return Invoice(user: user, purchases: purchases)
        }
    }
}

func processInvoices(userIds: [String]) async {
    let generator = InvoiceGenerator()
    
    // ✅ One call instead of multiple
    let invoices = await generator.generateInvoices(for: userIds)
    
    for invoice in invoices {
        print("Generated invoice for \(invoice.user.name)")
    }
}

Why Is This Better?

  • Only one cross-actor call is made, reducing overhead.
  • Cached profiles are accessed locally, avoiding unnecessary work.
  • Missing profiles are fetched in one batch, improving performance.

Conclusion

Actors preventing race conditions by controlling access to shared state but should be used wisely to avoid performance obstruct. We should keep frequently accessed data inside the actor and minimize cross-actor calls. Using global actors when a task must always run in the same execution context. With these kind of practices, we can write safe and efficient Swift concurrency code.

Use CaseRecommended Actor
UI updates in SwiftUI@MainActor
Background processingRegular actor
Logging, global stateCustom @GlobalActor

Sources

For further reading and to dive deeper into Swift actors and concurrency, you can explore the following resources:

Categorized in:

Swift,