Question About Actor Isolation in WWDC Video

I was watching the WWDC Video, “Protect Mutable State with Swift Actors”, in which the second speaker discusses actors and protocol conformances.

The speaker presents the following code example:

actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
}

extension LibraryAccount: Equatable {
    static func == (lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
        lhs.idNumber == rhs.idNumber
    }
}

The speaker then states that == is not isolated to the actor. What does “not isolated to the actor” mean? The speaker goes on to say, “we have two parameters of actor type, and this static method is outside of both of them.“ I know there’s answer to this here, but I still don’t understand what it means to be “not isolated to the actor”.

The speaker contrasts this with another example,

extension LibraryAccount: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(idNumber)
    }
}

The speaker states that this conformance isn’t allowed and that conforming to Hashable this way means this function could be called from outside the actor. I basically feel like I don’t understand what actor isolation means, even though I’ve read the proposal a number of times.

I am no expert, but it most likely means this. Because the function is static, and it is not attached to an isolation domain, it will always run in the caller’s execution context.

It means when you invoke a function isolated to an actor, the function will run in that actor’s execution context. As you most likely already know, an actor has a message queue (or mailbox) that receives the execution requests which the actor handles one at a time in its execution context.

2 Likes

Okay, and what is an execution context? (please explain in layman’s terms if possible)

Okay, so this implies that non-actor types also have their own execution contexts, since they also need to process their own requests.

An environment consisting of some state plus a thread.

@jamieQ , @John_McCall : please help us here. :confused:

1 Like

I think it might be a little misleading to talk about actors as providing an “execution context”. That’s a term I would associate more with a thread or a task.

What actors provide is isolated access to the data they protect. That isolation is typically associated statically with a particular scope in your code, like the body of an actor function. These scopes must execute with that specific actor isolated, so when a task enters such a scope, it tries to isolate that actor, waiting (if necessary) until it’s available. The task later gives up that isolation when it enters a scope that requires a different isolation; it only ever runs with at most one actor isolated dynamically.

A function that isn’t isolated to some specific actor (i.e. a nonisolated function) generally just picks up the isolation of its caller dynamically. Statically, Swift doesn’t know what that isolation is, so it won’t let you do anything that would require some specific isolation. That’s why your static function can’t read isolated properties from either of its actor arguments: you might call it from a function that has one of those actors isolated, but you also might not, and the static function wouldn’t know that in any case. (And since a task can only have one actor isolated at once, it could never have both arguments isolated, which it would need to in order to compare their properties.)

But a nonisolated function does normally run with the right isolation dynamically, which means it’s fine for its caller to pass in data that wouldn’t otherwise be shareable, like non-sendable data that’s stored on the actor. Since the function will only ever run while the actor is isolated, it can always safely access that data.

The exception is when a function is explicitly @concurrent, in which case it really does run with no isolation dynamically. Swift will prevent you from sharing non-sendable actor-isolated data with such a function.

8 Likes

I think you mean a nonisolated(nonseding) functions here. nonisolated functions are equivalent to @concurrent functions and they don't inherit any isolation.

1 Like

nonisolated(nonsending) exists only for transitioning to NonisolatedNonsendingByDefault. Once you enable that upcoming feauture (which is included in the Approachable Concurrency features), then yes, all nonisolated functions run wherever they're called from.

6 Likes

I hope that this isn’t being pedantic here, but if you are new to Swift actors, this can be surprising. Swift actors are reentrant – multiple actor methods can be in flight at the same time if some are marked as “async”. Actor methods marked as async can be suspended at await calls while other actor methods proceed. This differs from classic Erlang-style actors that process one message from their mailboxes at time. This was a conscious design decision and is well documented but can bite the unwary who think of actors as being completely protected from simultaneous access. Also, unlike some actor systems, Swift's actor "mailbox" system is opaque to application code.

4 Likes