ios 为什么在使用TaskGroup时收到有关类的参与者隔离属性的错误

0ejtzxu1  于 2023-03-20  发布在  iOS
关注(0)|答案(2)|浏览(103)

我正在学习Swift中的并发,我感到困惑。我很可能会有后续问题,所以请耐心等待。问题是:
我想做一个简单的函数,只上传那些遵循一定顺序的图像。我尝试使用任务组,这样我就可以在所有子任务完成后返回到挂起点。但是,我遇到了一个错误,我不明白。

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

我有一些与此错误相关的问题。
1.**为什么SKScene子类会引发此错误?**如果不创建SKScene子类,此错误将消失。SKScene有什么特别之处会引发此错误?
1.**Actor在哪里,为什么只有Task Groups?**这不是一个类吗?我想它可能会与“哦,任务必须保证某某事情”有关,但是当我将withTaskGroup(of:_:)切换为常规Task { }时,这个错误再次消失。所以我不确定为什么只有Task Groups会发生这种情况。
1.**我可以关闭Actor-isolation吗?**在这个示例中,我不关心images数组的线程安全。
1.**我可以减轻编译器对它作为inout传递的担忧吗?**既然我知道这个函数不会改变images的值,有什么方法可以减轻编译器对“不要作为inout传递作用器隔离属性”(有点像对结构体使用nonmutating关键字)的担忧吗?
1.**没有任何写入,竞态条件如何可能?**即使这是一个演员,为什么对一个数据块进行多次读取会有问题(我认为至少有一个任务也应该尝试写入数据)如果我能保证它永远不会尝试读取一段已经不存在的数据,或者保证对images数组的每次访问都是串行的,会怎么样呢?我能回避一下演员隔离吗?
如果其中任何一个问题本身需要一个新的问题,请让我知道。请记住,“隔离”,“非隔离”,“并发域”,“可发送闭包”,“@unchecked”,对我来说仍然是大而可怕的词,所以我可能会在我的回复中使用错误。如果我这样做了,请纠正我。
先谢了。

k97glaaz

k97glaaz1#

您可以通过向任务组提供所讨论的数组的副本来避免这个问题,从而避免从withTaskGroup闭包中访问类的属性。
一种简单的方法是使用捕获列表,替换:

await withTaskGroup(of: Void.self) { group in
    …
}

其中:

await withTaskGroup(of: Void.self) { [images] group in
    …
}

注意,通常不需要担心这个“复制”过程的效率(无论是通过捕获列表还是通过显式地将值类型数组赋值给一个新的局部变量来实现),因为它巧妙地在后台使用了“写入时复制”机制,对应用程序开发人员完全透明。
使用“copy on write”,它实际上只在必要时复制数组(即,您修改或“写入”其中一个数组),否则会提供类似于“copy”的语义,而不会产生实际复制整个集合的成本。
您还可以通过让编译器知道原始数组不能变异来避免此问题,例如替换:

var images = […]

let images = […]

显然,只有当images确实不可变时,才能这样做。

o7jaxewo

o7jaxewo2#

为什么SKScene子类会引发此错误?
演员在哪里?
如果你沿着继承层次向上看,你会发现SKScene最终继承自UIResponder/NSResponder,后者用一个全局参与者MainActor标记,参见它的声明here

@MainActor class UIResponder : NSObject

这就是actor所在的位置,因为类也继承自SKScene,而SKScene最终继承自UIResponder,所以类也与全局actor隔离。
为什么只有任务组?
这不仅仅是任务组,更简单的方法是:

func foo(x: () -> Void) {
    
}

func uploadCheckedImages() async {
    foo {
        let image = images[0]
    }
}

我可以减轻编译器对它作为inout传递的担忧吗?
是的,实际上有很多方法.一种方法是复制数组:

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

如果可以的话,将images设置为let常量也是可行的。
在没有任何写入的情况下,争用条件如何可能?
我认为编译器在这里限制太多了。这可能是故意的,也可能不是故意的。看起来它对闭包中捕获的每个l值都报告了一个错误,即使它没有被写入。这个错误应该在situations like this中触发。
你的代码很好,如果你添加一个identity函数,并把所有的左值表达式传递给这个函数,这样它们在编译器看来就不再像左值了,那么编译器就完全可以处理它,即使函数上没有任何差别。

// 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])

相关问题