Swift 5.5中的Actor
在Swift stdlib中实现了Actor隔离和可重入性。因此,苹果建议使用该模型来处理具有许多新并发功能的并发逻辑,以避免数据竞争。现在我们有了比基于锁的同步更清晰的替代方法(没有很多样板代码)。
一些UIKit
类,包括UIViewController
和UILabel
,现在支持@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
关键字与
class
或
struct
互换使用,我建议仅在严格需要并发性的实例中限制使用该关键字。使用该关键字会增加实例创建的额外开销,因此当没有共享状态需要管理时没有意义。
如果您不需要共享状态,则不要不必要地创建它。
struct
实例创建非常轻量级,大多数情况下最好创建一个新实例。例如:
SwiftUI
。
@MainActor
注释总体上是正确的答案,但你也可以使用await MainActor.run { .... }
从另一个 actor 上运行特定的代码块在主 actor 上。 - Bill