我已将 Strict Concurrency Checking 设置为 Complete,并且以下代码在 Xcode 15.0.1 和 Xcode 15.1 beta 3 中编译时不会出现警告。
运行时显示并发问题。
inc()
方法允许同时运行两个任务。
我的问题:
Task
中的run()
。但也许非 MainActor inc()
方法不应该从 MainActor 同步调用。class Counter {
private var counter = 0
func inc() -> Int {
let v = counter + 1
for _ in 0...1000 {}
counter = v
return v
}
@MainActor
func run() {
Task {
while true {
try? await Task.sleep(for: .seconds(1))
let v = inc()
print("xxx t1", v)
}
}
}
}
class Experiment {
func start() {
Task {
let counter = Counter()
await counter.run()
while true {
try? await Task.sleep(for: .seconds(1))
let v = counter.inc()
print("xxx t2", v)
}
}
}
}
你问:
- 我是否正确地假设这不应在没有警告的情况下编译?
我可以理解为什么您会期望出现编译时警告。此代码不是线程安全的。
FWIW,我经历的行为(在 Xcode 15.0.1 和 15.1 Beta 3 中)如下:
如果我删除
@MainActor
方法上的 run
隔离,则在使用“完整”的“严格并发检查”构建设置时会收到相应的编译时错误:
在“@Sendable”闭包中使用不可发送类型“Counter”捕获“self”。
“严格并发检查”构建设置控制您将收到的编译时警告。它主要是检查跨越任务边界的对象是否
Sendable
。 (对于那些不熟悉这些问题的人,WWDC 2022 视频使用 Swift Concurrency 消除数据竞争是一个很好的入门读物。)
所以这个警告是正确的:
Counter
是不可发送的。这段代码不是线程安全的。
但是,如果我恢复
@MainActor
的run
隔离,如原始示例所示,编译器无法生成相关警告(即使代码仍然不是线程安全的;Counter
仍然是非-)可发送)。
话虽如此,您报告了运行时错误。当我打开 Thread Sanitizer 时,我只看到运行时错误。顺便说一句,这就是“严格并发检查”的编译时警告比运行时检查好得多的原因:许多线程不安全行为在运行时并不总是一致表现出来。 “严格并发检查”可以检测在运行时可能难以显现的线程安全错误。
但是回到你的问题,目前还不清楚为什么编译器在仅存在
Counter
方法的 @MainActor
隔离的情况下无法检测到 run
的不可发送性。仅隔离这一个函数使得 Counter
并不比没有 @MainActor
隔离更线程安全。
你接着问:
- 这段代码哪部分是非法的? …
无论
run
是否与主要参与者隔离,问题在于这段代码根本就不是线程安全的。 Counter
不是Sendable
。它公开了 inc
函数,该函数正在改变非隔离属性,这意味着当 run
启动的任务正在执行时,另一个线程可以并行调用 inc
。 (或者,多个线程可以同时调用 inc
,无论是否曾经调用过 run
。)此代码不是线程安全的。
有多种方法可以修复此代码。我们希望将
Counter
设为 Sendable
类型。您可以将整个 Counter
类型隔离为 @MainActor
。或者把这个 class
变成 actor
。但是,就目前情况而言,Counter
不是线程安全的。