Search code examples
swiftasync-awaitactorswift-concurrency

Can an actor method be interacted concurrently?


I'm watching this WWDC video about Protect mutable state with Swift actors

and in one example they show how an actor method can be called concurrently

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.

enter image description here

Isn't the whole idea of actor is that it ensures that only one caller can directly interact with the actor at any given time?

Here is my example. Here you can see that "BEGIN increment" is always followed by "END increment" and that subsequent call to increment must await

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")
        }
    }
}

Solution

  • The problem in the video is that the actor function is asynchronous (an async method) with an await in it. The actor guarantees that only one synchronous function of the actor will be running at a time.

    Apple's warning is about adding async methods to your Actor. An await in an async method is an explicit indicator that you are willing to let the system suspend the current thread and give up the actor's context. This can allow another task to claim that context and "reenter" the function.

    It also means that any conclusions you come to about the actor's state before the await cannot be assumed true in code after the await.

    In your example, your increment function is synchronous (not marked async). So there is no point in your function where you suspend the actor context thereby allowing another thread to claim it.

    To put it another way the guarantees of an actor are implemented using Swift Concurrency. You can spoil those guarantees, and open a crack where trouble can creep in, if you use Swift Concurrency to implement async methods on the actor.