仅使用 @MainActor 注解类的一部分时的并发问题

问题描述 投票:0回答:1

我已将 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)
            }
        }
    }
}
swift concurrency task mainactor
1个回答
0
投票

你问:

  • 我是否正确地假设这不应在没有警告的情况下编译?

我可以理解为什么您会期望出现编译时警告。此代码不是线程安全的。

FWIW,我经历的行为(在 Xcode 15.0.1 和 15.1 Beta 3 中)如下:

  1. 如果我删除

    @MainActor
    方法上的
    run
    隔离,则在使用“完整”的“严格并发检查”构建设置时会收到相应的编译时错误:

    在“@Sendable”闭包中使用不可发送类型“Counter”捕获“self”。

    “严格并发检查”构建设置控制您将收到的编译时警告。它主要是检查跨越任务边界的对象是否

    Sendable
    。 (对于那些不熟悉这些问题的人,WWDC 2022 视频使用 Swift Concurrency 消除数据竞争是一个很好的入门读物。)

    所以这个警告是正确的:

    Counter
    是不可发送的。这段代码不是线程安全的。

  2. 但是,如果我恢复

    @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
不是线程安全的。

© www.soinside.com 2019 - 2024. All rights reserved.