错误提示您的类具有可变属性。该可变属性可以从Swift并发之外访问,因此不安全。
考虑以下内容:
final class Foo: Sendable {
@MainActor var counter = 0
}
我们现在可以考虑一个视图控制器与
counter
直接交互的以下属性和方法:
let foo = Foo()
func incrementFooManyTimes() {
DispatchQueue.global().async { [self] in
DispatchQueue.concurrentPerform(iterations: 10_000_000) { _ in
foo.counter += 1
}
print(foo.counter)
}
}
注意:如果您已将“Swift并发检查”构建设置设置为“Minimal”或“Targeted”,则上述代码只会编译出现上述警告。(如果更改为“Complete”,它将成为一个严重错误。)
简而言之,您已经将其标记为@MainActor,但没有阻止其他线程直接与此类的属性交互。要使类型为Sendable,它必须是以下之一:
- 不可变的;
- 手动同步其属性;或者
- 是actor
如果您要使用具有可变属性的非actor作为Sendable,则必须自己实现线程安全性。例如:
final class Foo: @unchecked Sendable {
private var _counter = 0
private let queue: DispatchQueue = .main
var counter: Int { queue.sync { _counter } }
func increment() {
queue.sync { _counter += 1 }
}
}
And
func incrementFooManyTimes() {
DispatchQueue.global().async { [self] in
DispatchQueue.concurrentPerform(iterations: 10_000_000) { _ in
foo.increment()
}
print(foo.counter)
}
}
显然,你也可以限制自己只使用不可变属性,这样就不需要同步了。但我假设你需要可变性。
在这种可变的情况下,你可以使用任何同步机制,但希望这说明了这个概念。简而言之,如果你要允许它在Swift并发之外进行突变,你必须自己实现同步。并且由于我们正在实现自己的同步,我们告诉编译器它是@unchecked
,这意味着你不会让编译器检查其正确性,而是这个负担落在你的肩上。
显然,如果你使用一个actor并留在Swift并发的世界中,生活会轻松得多。例如:
actor Bar {
var counter = 0
func increment() {
counter += 1
}
}
同时:
let bar = Bar()
func incrementBarManyTimes() {
Task.detached {
await withTaskGroup(of: Void.self) { group in
for _ in 0 ..< 10_000_000 {
group.addTask { await self.bar.increment() }
}
await print(self.bar.counter)
}
}
}
concurrentPerform
的代码片段在我的电脑上无法编译。这里的self
是什么?这个函数应该在一个类中吗?当counter
被隔离到@MainActor
时,如何访问它? - Sweeperself
是一个视图控制器,“incrementFooManyTimes”只是一个随机的方法。关于“如何访问计数器”的问题,那就是我的全部观点。如果你在一个随机类内有一个@MainActor
属性,非隔离代码(比如我在视图控制器中的随机方法)仍然可以访问和与其交互。当然,如果你从Swift并发性访问它,编译器将执行严格的演员隔离规则,但如果没有这样做,@MainActor
就不提供任何功能上的好处。但如果它是一个演员(像我的Bar
示例),那么编译器将强制执行所有必要的检查。 - RobUIViewController
中,它可以编译通过了。虽然我完全没有想到会是这样。如果它是在一个没有超类或被标记为@MainActor
的类中,它就不会起作用。我想知道 UIKit 类型有什么特别之处...无论如何,总的来说,我同意你的观点。 - Sweeper