3

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

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

// 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)
   }
}

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

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

4

1 回答 1

10

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

一些UIKit类,包括UIViewControllerUILabel,现在对@MainActor. 所以我们只需要在自定义UI相关类中使用注解即可。例如,在上面的代码中,myImageView.image会自动在主队列上调度。但是,UIImage.init(named:)调用不会在视图控制器之外的主线程上自动分派。

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

解决方案 1

最简单的可能。此属性在与 UI 相关的类中可能很有用。@MainActorApple 使用方法注释使该过程更加简洁:

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

这段代码等效于 wrap in DispatchQueue.main.async,但调用站点现在是:

await setImage(thumbnailName: "thumbnail")

解决方案 2

如果你有自定义 UI 相关的类,我们可以考虑应用@MainActor到类型本身。这确保所有方法和属性都在 main 上调度DispatchQueue

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

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

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

await当我们onButtonTapactor.

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

我们还可以在一个外部的主线程上调用函数actor

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

在不同的内部actor

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

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

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

此解决方案比上述两个最适合将整个函数包装在MainActor. 但是,actor.run也允许actors 之间的线程间代码合而为一func(感谢@Bill 的建议)。

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

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

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

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

概括

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

奖金票据

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

如果您不需要共享状态,则不要不必要地创建它。struct实例创建是如此轻量级,以至于大多数时候创建一个新实例会更好。例如SwiftUI

于 2021-06-08T19:23:00.330 回答