Issue #905

Protect mutable state with Swift actors

Actor reentrancy

Imagine we have two different concurrent tasks trying to fetch the same image at the same time. The first sees that there is no cache entry, proceeds to start downloading the image from the server, and then gets suspended because the download will take a while. While the first task is downloading the image, a new image might be deployed to the server under the same URL. Now, a second concurrent task tries to fetch the image under that URL. It also sees no cache entry because the first download has not finished yet, then starts a second d ownload of the image. It also gets suspended while its download completes.

After a while, one of the downloads – let’s assume it’s the first – will complete and its task will resume execution on the actor. It populates the cache and returns the resulting image of a cat. Now the second task has its download complete, so it wakes up. It overwrites the same entry in the cache with the image of the sad cat that it got. So even though the cache was already populated with an image, we now get a different image for the same URL.

Swift actors: How do they work, and what kinds of problems do they solve?

Because even though each actor does indeed serialize all calls to it, when an await occurs within an actor, that execution is still suspended just like any other — meaning that the actor will be unblocked and ready to accept new requests from other code

Actor reentrancy occurs if there’s suspension point with await inside the actor. In the below example, there is no suspension point

actor Counter {
    var count = 1

    func increment() {
        print("BEGIN increment")
        let url = URL(string: "https://google.com")!
        let data = try! Data(contentsOf: url)
        let string = String(data: data, encoding: .utf8) ?? ""
        print("END increment")
        count += 1
    }
}


struct ContentView: View {
    @State var counter = Counter()

    var body: some View {
        Button {
            Task.detached {
                await counter.increment()
            }
        } label: {
            Text("Click")
        }
    }
}

The Actor Reentrancy Problem in Swift

Even though the reentrancy problem is happening in a multithreaded context, it doesn’t mean that it is a thread safety issue. The reentrancy problem occurs because we are assuming that the actor state will not change across a suspension point, not because we are trying to change the actor mutable state concurrently. Therefore, a reentrancy problem is not equivalent to a thread-safety problem.

Actor reentrancy

Actor reentrancy prevents deadlocks and guarantees forward progress, but it requires you to check your assumptions across each await.

Reentrancy tips:

Perform mutation of actor state within synchronous code. Ideally, do it within a synchronous function so all state changes are well-encapsulated State changes can involve temporarily putting our actor into an inconsistent state - make sure to restore consistency before an await Expect that the actor state could change during suspension - all awaits are potential suspension points Check your assumptions after resuming (after an await)

Eliminate data races using Swift Concurrency

But these are awaits, meaning that our task could get suspended here and the actor could do other higher-priority work, like battling pirates.

In this specific case, the Swift compiler will reject an attempt to outright modify the state on another actor.

When you are writing your actor, think in terms of synchronous, transactional operations that can be interleaved in any way.

Every one of them should ensure that the actor is in a good state when it exits.

How to use @MainActor to run code on the main queue

In fact, this set up is so central to the way ObservableObject works that SwiftUI bakes it right in: whenever you use @StateObject or @ObservedObject inside a view, Swift will ensure that the whole view runs on the main actor so that you can’t accidentally try to publish UI updates in a dangerous way. Even better, no matter what property wrappers you use, the body property of your SwiftUI views is always run on the main actor.

Read more