我正在学习 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))
}
}
我有一些与此错误相关的问题。
为什么 SKScene 子类会引发此错误? 当我不对 SKScene 进行子类化时,此错误就会消失。引发此错误的 SKScene 有何特别之处?
Actor 在哪里,为什么只有任务组? 这不是一个类吗?我认为它可能必须与“哦,任务必须保证某某事”做某事,但是当我将
withTaskGroup(of:_:)
切换为常规Task { }
时,此错误再次消失。所以我不确定为什么这只发生在任务组中。
我可以关闭 Actor-isolation 吗? 在这种情况下,我不关心我的
images
数组的线程安全。
我可以减轻编译器对它作为 inout 传递的担忧吗? 因为我知道这个函数不会改变
images
的值,有什么办法可以减轻编译器对“不通过”的担忧actor-isolated properties as inout”(有点像对结构使用 nonmutating
关键字)?
没有任何写入的竞争条件怎么可能? 即使这是一个演员,为什么多次读取一条数据会成为一个问题(我认为至少有一个任务应该尝试将数据写入为出色地)?如果我能保证它永远不会尝试读取一条不再存在的数据会怎么样。或者保证对
images
数组的每次访问都是串行的。我能解决这个演员隔离问题吗?
如果其中任何一个本身需要一个新问题,请告诉我。请记住“隔离”、“非隔离”、“并发域”、“可发送闭包”、“@unchecked”对我来说仍然是又大又吓人的词,所以我可能会在回复中错误地使用它们。如果我这样做,请纠正我。
提前致谢。
为什么
子类会引发此错误?SKScene
演员在哪里?
如果向上继承层次结构,您会看到
SKScene
最终继承自 UIResponder
/NSResponder
,后者标有全局角色 - MainActor
。从它的声明中看到这里.
@MainActor class UIResponder : NSObject
那是演员所在的地方。由于您的班级也继承自
SKScene
,最终继承自UIResponder
,因此您的班级也与全球演员隔离。
为什么只有任务组?
不仅仅是任务组。重现这个的更简单的方法是:
func foo(x: () async -> 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
常数也可以,如果你能做到的话。
没有任何写操作怎么可能出现竞争条件?
我认为编译器在这里限制太多了。这可能是有意的,也可能不是。它似乎在为闭包中捕获的每个左值报告一个错误,即使它没有被写入。这个错误应该在像这样的情况下触发。
你的代码没问题。如果你添加一个
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])
你可以通过给你的任务组一个有问题的数组的副本来避免这个问题,避免从
withTaskGroup
闭包中访问类的属性。
一个简单的方法是使用捕获列表,替换:
await withTaskGroup(of: Void.self) { group in
…
}
搭配:
await withTaskGroup(of: Void.self) { [images] group in
…
}
注意,通常不需要担心这个“复制”过程的效率(无论是通过捕获列表还是通过将值类型数组显式分配给新的局部变量),因为它巧妙地使用了“复制写”机制在幕后,对应用程序开发人员完全透明。
使用“写时复制”,它实际上只在必要时复制数组(即,您改变或“写入”其中一个数组),否则会为您提供类似“复制”的语义,而不会产生实际复制整个数组的成本收藏
你也可以通过让编译器知道原始数组不能改变来避免这个问题,例如,替换:
var images = […]
与
let images = […]
很明显,只有当
images
真的是不可变的时候,你才能这样做。