Swift actor isolated vs nonisolated

Apr 02, 2023#swift#concurrency

Swift actor isolation is a feature of Swift concurrency that allows actors to protect their state from concurrent access by other actors or threads. Actor isolation ensures that only one actor can access its own state at a time, and that any communication with other actors is done asynchronously through messages. This helps prevent data races and other concurrency bugs.

By default, all properties and methods of an actor are isolated to that actor, which means they can only be accessed from within the actor or by using await from outside the actor. There are some exceptions to actor isolation, such as accessing immutable state or using nonisolated keywords to mark methods that can be safely called from outside the actor.

actor Employee {
    let employeeID: Int
    var salary: Double

    init(employeeID: Int, initialSalary: Double) {
        self.employeeID = employeeID
        self.salary = initialSalary
    }
    
    func increaseSalary(amount: Double) {
        // Accessed from within the actor without await
        assert(amount >= 0)
        salary += amount
    }
}

The isolated and nonisolated keywords in Swift are used to control the actor isolation of functions and properties.

isolated

The isolated keyword can only be used on function parameters that are actor types. This means that the function can access the actor’s state without using await, as if it were part of the actor itself.

func giveRaise(to employee: isolated Employee, amount: Double) async {
    employee.increaseSalary(amount: amount)
}

A given function cannot have multiple isolated parameters:

// warning: cannot have more than one 'isolated' parameter; this is an error in Swift 6.
func f(a: isolated Employee, b: isolated Employee) {  
    // ...
}

The isolated keyword cannot be used on properties or methods of actors. For example:

actor Employee {
    /*...*/

    // error: 'isolated' may only be used on 'parameter' declarations
    isolated var x = 0

    // error: 'isolated' may only be used on 'parameter' declarations
    isolated func baz() {
        // do something
    }
}

nonisolated

Sometimes you may want to mark some properties or methods as nonisolated, which means they can be accessed without await from any context. This is useful when the property or method does not depend on or affect the mutable state of the actor.

actor Employee {
    // ...
    nonisolated let age: Int

    // A nonisolated method that can be called from any context
    nonisolated func doSomething() {
        print("Do something without touch actor state")
    }
}

// A global function that takes an actor as a parameter
func test(_ employee: Employee) async {
    // Call the nonisolated method without await
    employee.doSomething()

    // Call the isolated method with await
    await counter.increaseSalary(amount: 10)
}

Non-isolated functions don’t have access to actor state, so they are free to satisfy synchronous protocol requirements of any kind. For example:

extension Employee: Hashable {
    nonisolated func hash(into hasher: inout Hasher) {
        hasher.combine(employeeID) 
    }  
}