Swift 并发:为什么这个方法是异步的?

Dor*_*Roy 3 swift swift-concurrency

我正在尝试思考如何将 Swift 并发性与使用基于块的东西(如 Timer)的旧代码集成。因此,当我构建下面的代码时,编译器告诉我self.handleTimer()该行Expression is 'async' but is not marked with 'await'

为什么是异步的?它没有被标记async并且没有做任何事情。当我在没有计时器的情况下调用它时,我不需要await. 参与者隔离是否意味着对成员的每次调用都是来自该上下文之外的“异步”?

@MainActor
class MyClass {
    
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            Task {
                self.handleTimer() // "Expression is 'async' but is not marked with 'await'"
            }
        }
    }
    
    func handleTimer() {
    }
}
Run Code Online (Sandbox Code Playgroud)

jrt*_*ton 5

参与者隔离是否意味着对成员的每次调用都是来自该上下文之外的“异步”?

是的。

您可以通过告诉任务在主要参与者上完成工作来删除警告,如下所示:

Task { @MainActor in
    self.handleTimer()
}
Run Code Online (Sandbox Code Playgroud)

或者标记handleTimer()nonisolated,如果它没有会破坏参与者隔离的副作用。


mat*_*att 5

我认为理解这种情况的方法是分阶段进行构建。让我们从没有async/await任何类型的标记开始:

\n
class MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n            self.handleTimer()\n        }\n    }\n    func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

好吧,我们都知道我们可以这样说话;我们已经这样做很多年了。现在让我们将 MyClass 标记为@MainActor

\n
@MainActor\nclass MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n            self.handleTimer() // warning -> error\n        }\n    }\n    func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在self.handleTimer()引发了一个警告,我们被告知,当 Swift 并发在 Swift 6 中完全实现时,该警告将演变成一个错误:“在同步非隔离上下文中调用主参与者隔离实例方法‘handleTimer()’。”

\n

显然,编译器对这一行持怀疑态度。但什么?正如您自己所说,handleTimer似乎无害。但让我们退后一步。这些年来,我们已经习惯了这样的观念:事情通常倾向于“在主线程上”发生。只要这是普遍正确的,就不存在任何线程问题。如果我们打电话handleTimer从视图控制器调用,一切都很好:

\n
class ViewController: UIViewController {\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        MyClass().handleTimer()\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

没问题。但没那么快。没有问题,只是因为ViewController作为UIViewController,被标记了@MainActor(隐式的)。一旦我们引入一个没有这样标记的类,事情就会很快崩溃:

\n
class OtherClass {\n    func test() {\n        MyClass().handleTimer() // BLAAAAAAAAAAP!\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

哇!事实证明,这条线路上的一切都是非法的。我们不仅不能调用handleTimer,甚至不能调用 MyClass 初始化器!

\n

为什么这里有问题呢?让我们从历史的角度来看待这个问题。当你熟睡时,Apple 悄悄地@MainActor为你最喜欢的所有 UIKit 类添加了一个名称,并开启了 Swift 并发性的开端。但由于 Apple 将此名称附加到所有您最喜欢的 UIKit 类中,您甚至没有注意到;它只是一堆在同一个参与者(即主要参与者)上相互调用的方法,因此编译器非常高兴。

\n

然而,在这样的世界中,我们的其他类是一个陌生人。它没有声明@MainActor。因此,当它与 MyClass 对话时,它会跨越 actor 上下文\xe2\x80\x94,编译器会向我们施加其全部愤怒。

\n

显然我们可以通过将OtherClass带入我们的快乐@MainActor世界来解决这个问题:

\n
@MainActor\nclass OtherClass {\n    func test() {\n        MyClass().handleTimer() // no problem :)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我们也可以通过引入 full-on 来解决这个问题async/await,当你想要跨 Actor 上下文进行对话时,你可以这样做:

\n
class OtherClass {\n    func test() {\n        Task {\n            await MyClass().handleTimer()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在该代码中,我们保证 OtherClass 绑定到哪个 actor(如果有),或者 Task 绑定到哪个 actor(如果有)。但是,我们可以通过正确地跨越参与者上下文,即通过awaitasync上下文世界(即任务)中说出来来表现得连贯。

\n

好的!现在我们准备好解决您最初的问题了。正如我们所展示的,一切都很好,直到您@MainActor在 MyClass 上说。这时,一个问题就出现了:该台词的演员身份是什么self.handleTimer()

\n
@MainActor\nclass MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n            self.handleTimer() // warning -> error\n        }\n    }\n    func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

编译器说:“这一行属于 Timer(和运行时),而不属于您。这个self.handleTimer()调用来自MyClass外部。但是 MyClass 绑定到主要参与者,所以您不能这样做!我(编译器)我现在会原谅你,因为如果我不这样做,你所有现有项目中每个 UIKit 类中的所有 Timer 代码都会中断。但我警告你,我不会那么容易 -未来还要走!”

\n

为了解决这个问题,一种方法是保证主角self.handleTimer()本身。一种方法是将调用包装在 MainActor 块中。但我们不能在非异步上下文中这样做;例如,这根本无法编译:

\n
@MainActor\nclass MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n            MainActor.run { // No way!!!!\n                self.handleTimer()\n            }\n        }\n    }\n    func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

相反,我们必须利用刚刚学到的教训。我们需要进入异步上下文 \xe2\x80\x94,因为 Timer 块甚至不属于我们,所以我们只能通过引入任务来完成。然后我们可以将该任务的内部标记为与主要参与者相关,整个问题就消失了:

\n
@MainActor\nclass MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n            Task { @MainActor in\n                self.handleTimer()\n            }\n        }\n    }\n    func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

显然,另一种选择是,不用@MainActor在任务中说,而是使用完整的async/await谈话,这样我们就可以跨越参与者的上下文:

\n
@MainActor\nclass MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n            Task {\n                await self.handleTimer()\n            }\n        }\n    }\n    func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

最后但并非最不重要的一点是,我们可以像jrturton所说的那样:将handleTimer 自己标记为无害的。这很棘手,因为您告诉编译器您比它知道的更多,这总是有风险的(就像当您使用 进行强制转换时as!)。但由于handleTimer目前是空的,我们当然可以逃脱它:

\n
@MainActor\nclass MyClass {\n    func startTimer() {\n        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in\n                self.handleTimer()\n            }\n    }\n    nonisolated func handleTimer() {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n