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.
-
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?
-
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 regularTask { }
, this error again disappears. So I’m not sure why this is only happening with Task Groups. -
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 thenonmutating
keyword for structs)?
2
Answers
If you go up the inheritance hierarchy, you’d see that
SKScene
ultimately inherits fromUIResponder
/NSResponder
, which is marked with a global actor – theMainActor
. See from its declaration here.That’s where the actor is. Since your class also inherits from
SKScene
, which ultimately inherits fromUIResponder
, your class also gets isolated to the global actor.It’s not just task groups. A more minimal way to reproduce this is:
Yes, there are a lot of way, in fact. One way is to make a copy of the array:
Making
images
alet
constant also works, if you can do that.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.You can avoid this problem by giving your task group a copy of the array in question, avoiding accessing properties of the class from within the
withTaskGroup
closure.One easy way to do this is to use a capture list, replacing:
With:
Note, one generally does not need to worry about the efficiency of this “copy” process (whether achieved with the capture list or by explicitly assigning the value-type array to a new local variable), because it cleverly employs the “copy on write” mechanism behind the scenes, completely transparent to the application developer.
With “copy on write”, it really only copies the array if necessary (i.e., you mutate or “write” one of the arrays), and otherwise gives you “copy”-like semantics without incurring the cost of actually copying the whole collection.
You can also avoid this problem by letting the compiler know that the original array can not mutate, e.g., replacing:
With
Obviously, you can only do this if the
images
really is immutable.