我可以使用 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
类,包括 UIViewController
和 UILabel
,现在对 @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
关键字与 class
或 struct
互换使用,因此我建议将关键字限制在严格需要并发的情况下。使用关键字会增加实例创建的额外开销,因此在没有要管理的共享状态时没有意义。
如果您不需要共享状态,则不要不必要地创建它。 struct
实例创建非常轻量级,大多数情况下最好创建一个新实例。例如SwiftUI
.
我最近看到 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
类,包括 UIViewController
和 UILabel
,现在对 @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
关键字与 class
或 struct
互换使用,因此我建议将关键字限制在严格需要并发的情况下。使用关键字会增加实例创建的额外开销,因此在没有要管理的共享状态时没有意义。
如果您不需要共享状态,则不要不必要地创建它。 struct
实例创建非常轻量级,大多数情况下最好创建一个新实例。例如SwiftUI
.