我能使用Swift中的actors,在主线程上始终调用一个函数吗?

9

我最近看到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函数?

@MainActor 注释总体上是正确的答案,但你也可以使用 await MainActor.run { .... } 从另一个 actor 上运行特定的代码块在主 actor 上。 - Bill
1个回答

26

Swift 5.5中的Actor

在Swift stdlib中实现了Actor隔离和可重入性。因此,苹果建议使用该模型来处理具有许多新并发功能的并发逻辑,以避免数据竞争。现在我们有了比基于锁的同步更清晰的替代方法(没有很多样板代码)。

一些UIKit类,包括UIViewControllerUILabel,现在支持@MainActor。因此,在自定义UI相关类中只需使用注释即可。例如,在上面的代码中,myImageView.image将自动分派到主队列。但是,在视图控制器之外调用UIImage.init(named:)时不会自动分派到主线程。

在一般情况下,@MainActor对于并发访问UI相关状态非常有用,并且最容易使用,尽管我们也可以手动分派。下面我概述了可能的解决方案:

解决方案1

这是最简单的方式。此属性在UI相关类中非常有用。苹果使用@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(适用于块和函数)

我们也可以在主线程外部调用函数,使用以下代码:

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

在不同的演员(actor)里面:
func onButtonTap(...) {
    await MainActor.run { 
        ....
    }
}

如果我们想从MainActor.run中返回,只需在签名中指定即可:

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

这种解决方案不如前两种解决方案那么干净,前两种解决方案最适用于在MainActor上包装一个完整的函数。然而,actor.run也允许在一个func中的actor之间进行线程间代码交互(感谢@Bill的建议)。

解决方案4(块解决方案适用于非异步函数)

调度块到@MainActor的另一种方法是解决方案3的替代方案:

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

优点在于相比解决方案3,这里的封闭func不需要被标记为async。但请注意,与解决方案3立即分派块不同,这里稍后再分派块。

总结

演员使Swift代码更安全、更清洁、更易编写。不要过度使用它们,但将UI代码分派到主线程是一个很好的用例。请注意,由于此功能仍处于测试版,因此框架未来可能会有所改变/改进。
由于我们可以轻松地将actor关键字与classstruct互换使用,我建议仅在严格需要并发性的实例中限制使用该关键字。使用该关键字会增加实例创建的额外开销,因此当没有共享状态需要管理时没有意义。
如果您不需要共享状态,则不要不必要地创建它。struct实例创建非常轻量级,大多数情况下最好创建一个新实例。例如:SwiftUI

“不要过度使用它们”是什么意思?这可能会引起特定的问题吗? - deaton.dg
如果您有一个包含许多UI方法的视图控制器,我们可以考虑将@MainActor应用于类型本身。但这是错误的,视图控制器等已经绑定到主actor。 - matt
1
你可能想阅读我的 https://www.biteinteractive.com/swift-5-5-replacing-gcd-with-async-await/,了解演员是线程切换和数据锁定的基础。 - matt
太新鲜了!我是在 WWDC 21 视频发布当天写下这个答案的,他们的示例代码使用了 @MainActor class MyViewController: UIViewController。感谢您让我知道。 - Pranav Kasetti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接