Search code examples
iosswiftconcurrencytaskactor

Why am I getting an error about an actor isolated property on a class while using TaskGroup


I wanted to make a simple function that uploads only those images that follow a certain order. I tried using Task Groups for this as that way I can return back to the suspension point after all child Tasks have completed. However, I ran into an error I don't understand.

class GameScene: SKScene {
    var images = ["cat1", "mouse2", "dog3"]
    
    func uploadCheckedImages() async {
        await withTaskGroup(of: Void.self) { group in
            for i in images.indices {
                let prev = i == 0 ? nil : images[i - 1]  // << Error: Actor-isolated property 'images' cannot be passed 'inout' to 'async' function call 
                let curr = images[i]  // << Error: Actor-isolated property 'images' cannot be passed 'inout' to 'async' function call
                if orderIsPreserved(prev ?? "", curr) {
                    group.addTask { await self.uploadImage(of: curr) }
                }
            }
        }
    }
    
    func orderIsPreserved(_ a: String, _ b: String) -> Bool {
        return true
    }
    
    func uploadImage(of: String) async {
        try! await Task.sleep(for: .seconds(1))
    }
}

I have a handful of questions related to this error.

  1. Why does a SKScene subclass raise this error? When I don't subclass SKScene this error disappears. What's so special about SKScene that raises this error?

  2. Where is the Actor and why only Task Groups? Isn't this a class? I thought it may have to do something with "Oh a task has to guarantee so and so things" but when I switch withTaskGroup(of:_:) to a regular Task { }, this error again disappears. So I'm not sure why this is only happening with Task Groups.

  3. Can I ease the compilers worries about it being passed as inout? Since I know that this function isn't altering the value of images, is there any way I can ease the compilers worries about "don't pass actor-isolated properties as inout" (sort of like using the nonmutating keyword for structs)?


Solution

  • Why does a SKScene subclass raise this error?

    Where is the Actor?

    If you go up the inheritance hierarchy, you'd see that SKScene ultimately inherits from UIResponder/NSResponder, which is marked with a global actor - the MainActor. See from its declaration here.

    @MainActor class UIResponder : NSObject
    

    That's where the actor is. Since your class also inherits from SKScene, which ultimately inherits from UIResponder, your class also gets isolated to the global actor.

    why only Task Groups?

    It's not just task groups. A more minimal way to reproduce this is:

    func foo(x: () -> Void) {
        
    }
    
    func uploadCheckedImages() async {
        foo {
            let image = images[0]
        }
    }
    

    Can I ease the compilers worries about it being passed as inout?

    Yes, there are a lot of way, in fact. One way is to make a copy of the array:

    func uploadCheckedImages() async {
        let images = self.images // either here...
        await withTaskGroup(of: Void.self) { group in
            // let images = self.images // or here
            // ...
        }
    }
    

    Making images a let constant also works, if you can do that.

    How is a race-condition possible without any writes?

    I think the compiler is just kind of being too restrictive here. This may or may not be intended. It seems like it's reporting an error for every l-value captured in the closure, even when it's not being written to. This error is supposed to be triggered in situations like this.

    Your code is fine. If you add an identity function and pass all the l-value expressions into this function, so they no longer look like l-values to the compiler, then the compiler is perfectly fine with it, even though there is absolutely no functional difference.

    // this is just to show that your code is fine, not saying that you should fix your code like this
    
    // @inline(__always) // you could even try this
    func identity<T>(_ x: T) -> T { x }
    
    await withTaskGroup(of: Void.self) { group in
        for i in images.indices {
            let prev = i == 0 ? nil : identity(images[i - 1])
            let curr = identity(images[i])