我可以使用 Swift 中的 actors 来始终调用主线程上的函数吗?

Can I use actors in Swift to always call a function on the main thread?

我最近看到 Swift 在 Swift 5.5 中引入了对 Actor 模型的并发支持。当我们有一个共享的、可变的状态时,这个模型启用安全的并发代码来避免数据竞争。

我想在我的应用 UI 中避免主线程数据争用。为此,我在设置 UIImageView.image 属性 或 UIButton 样式的调用站点包装 DispatchQueue.main.async

// Original function
func setImage(thumbnailName: String) {
    myImageView.image = UIImage(named: thumbnailName)
}

// Call site
DispatchQueue.main.async {
    myVC.setImage(thumbnailName: "thumbnail")
}

这似乎不安全,因为我必须记住在主队列上手动分派该方法。另一个解决方案如下:

func setImage(thumbnailName: String) {
   DispatchQueue.main.async {
      myImageView.image = UIImage(named: thumbnailName)
   }
}

但这看起来像很多样板文件,我不会说我喜欢将它用于具有不止一层嵌套的复杂函数。

发布 Swift 对 Actors 的支持看起来是一个完美的解决方案。那么,有没有办法让我的代码更安全,即始终使用 Actors 在主线程上调用 UI 函数?

演员Swift5.5‍♀️

Actor 隔离和重入现在在 Swift stdlib 中实现。因此,Apple 建议使用具有许多新并发功能的并发逻辑模型来避免数据竞争。我们现在有一个更简洁的替代方案,而不是基于锁的同步(大量样板文件)。

一些 UIKit 类,包括 UIViewControllerUILabel,现在对 @MainActor 提供开箱即用的支持。所以我们只需要使用custom UI-related 类中的注解即可。例如,在上面的代码中,myImageView.image 会自动分配到主队列中。但是,UIImage.init(named:) 调用不会在视图控制器外部的主线程上自动调度。

在一般情况下,@MainActor 对于并发访问 UI 相关状态很有用,并且是最容易做到的,即使我们也可以手动调度。我在下面概述了可能的解决方案:

解决方案 1

尽可能简单。此属性在 UI 相关的 类 中很有用。 Apple 使用 @MainActor 方法注释使过程更加清晰:

@MainActor func setImage(thumbnailName: String) {
    myImageView.image = UIImage(image: thumbnailName)
}

这段代码相当于在DispatchQueue.main.async中换行,但是现在的调用点是:

await setImage(thumbnailName: "thumbnail")

解决方案 2

如果您有自定义 UI 相关的 类,我们可以考虑将 @MainActor 应用于类型本身。这确保所有方法和属性都在主 DispatchQueue.

上调度

然后我们可以使用 nonisolated 关键字手动退出主线程,用于非 UI 逻辑。

@MainActor class ListViewModel: ObservableObject {
    func onButtonTap(...) { ... }

    nonisolated func fetchLatestAndDisplay() async { ... }
}

当我们在 actor.

中调用 onButtonTap 时,我们不需要明确指定 await

解决方案 3(适用于块和函数)

我们还可以在 actor 之外调用主线程上的函数:

func onButtonTap(...) async {
    await MainActor.run { 
        ....
    }
}

内部不同actor:

func onButtonTap(...) {
    await MainActor.run { 
        ....
    }
}

如果我们想从 MainActor.run 中 return,只需在签名中指定:

func onButtonTap(...) async -> Int {
    let result = await MainActor.run { () -> Int in
        return 3012
    }
    return result
}

这个解决方案比上面两个解决方案最适合更适合在[=35上包装整个函数=].但是,actor.run 还允许在一个 func 中的 actor 之间使用线程间代码(thx @Bill 的建议)。

解决方案 4(在非异步函数中工作的块解决方案)

@MainActor 上的块安排到解决方案 3 的另一种方法:

func onButtonTap(...) {
    Task { @MainActor in
        ....
    }
}

与解决方案 3 相比,这里的优势在于封闭的 func 不需要标记为 async。但是请注意,这会稍后分派块,而不是像解决方案 3 中那样立即

总结

Actor 使 Swift 代码更安全、更简洁、更易于编写。不要过度使用它们,但是将 UI 代码分派到主线程是一个很好的用例。请注意,由于该功能仍处于测试阶段,该框架可能 change/improve 将来会更进一步。

奖金

由于我们可以轻松地将 actor 关键字与 classstruct 互换使用,因此我建议将关键字限制在严格需要并发的情况下。使用关键字会增加实例创建的额外开销,因此在没有要管理的共享状态时没有意义。

如果您不需要共享状态,则不要不必要地创建它。 struct 实例创建非常轻量级,大多数情况下最好创建一个新实例。例如SwiftUI.