为了防止误解这个问题:我很清楚,@Published属性的publishing个新值是不允许的.This is not my question though.

I want to know if getting / reading a value of 100 property from the background thread (without changing it) is OK.如果不是,原因何在?

例如:

class ViewModel: ObservableObject {
    
    @Published var name: String = ""
    
    func saveName() {
        DispatchQueue.global().async {
            print("Getting name from background: \(self.name)") // <-- HERE
        }
    }
}

struct ContentView: View {
    
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            TextField("Name", text: $viewModel.name)
            Button("Submit") {
                viewModel.saveName()
            }
        }
    }
}

直觉上感觉不对劲,但是

  • 没有编译错误
  • 运行此类代码时未出现运行时错误
  • 没有看到任何副作用,而且
  • 文件中没有具体说明这是错误的

这是一个有效的代码吗?

推荐答案

你问:

那么这是一个有效的代码吗?

该代码隐含地提出了许多与线程安全不一致的声明.具体地说,就是:

  • 该属性是可变的(即,它是一个变量,而不是常量)…代码显然还没有真正改变它,但将它声明为变量表示它表示一种可变状态;

  • 该属性在不提供同步的类型内部(即,它不在"可发送"类型内);以及

  • GCD分派期望闭包"捕获"的任何东西都是线程安全的(即,闭包用@Sendable装饰,告诉编译器判断闭包捕获的任何东西是否真正"可发送";它必须是不可变的或同步的).

线程安全的全部思想是确保不会发生不同步的Mutations .这是一个有趣的学术问题,您是否希望将此示例称为"无效":从技术上讲,代码还没有做任何不安全的事情(即,没有Mutations ),但它仍然代表了一种不同步的、可变的状态.这可以通过使属性不可变(一个let常量)或通过使包含该属性的类型"可发送"来修复.

您注意到尚未收到编译错误.这是因为苹果在默认情况下禁用了大部分可发送性判断(即检测不同步、可变状态).这将在SWIFT 6中强制执行,但在5.x中,它仍然是一项可选判断.(这是一个有意识的决定,让我们有时间采用这些新模式,编写编译器可以正确验证线程安全性的代码.)

通过将"严格并发判断"构建设置设置为"完成",可以启用对非同步可变状态的检测.如果您这样做,Xcode将警告您这个问题:

enter image description here

如果您将ObservableObject设置为可发送,并将其隔离到主要演员(如WWDC2021的S视频Discover concurrency in SwiftUI中所建议的),您现在将从后台队列中收到有关访问name的警告,现在将隔离到主要演员:

enter image description here

如果您有一个要发布到UI的属性,最简单的方法是将具有@Published属性的整个类型隔离到主要参与者,并消除GCD的使用.例如,假设您想要保存名字,但因为这可能需要几毫秒以上的时间,所以您可能会将其从主要参与者中移出.您只需将此参与者隔离属性作为参数提供给在其他参与者上运行的函数:

@MainActor
class ViewModel: ObservableObject {
    @Published var name: String = ""

    let databaseManager = DatabaseManagerOnOtherActor()

    func saveName() async throws {
       try await databaseManager.save(name)
    }

    func loadName() async throws {
       name = try await databaseManager.name()
    }
}

actor DatabaseManagerOnOtherActor {
    func save(_ name: String) throws { … }

    func name() throws -> String { … }
}

因此,我们避免阻塞主线程,这是线程安全的,但不需要GCD(事实上,它通常应该与async-await一起避免).

总而言之,你认为有些地方不对劲的直觉是正确的.编译时判断的目的不是通知您您实际上已经表现出了一种竞争,而是至少警告您很容易受到这种竞争的影响.我们的 idea 是,如果我们观察到所有的警告,我们就不会出现低水平的数据竞赛.

WWDC 2022视频,Eliminate data races using Swift Concurrency,是这个主题的一个很好的入门读物(如果是基本的话).

Swift相关问答推荐

在Swift中initt()完成后运行一个函数

无法创建MKAssociateRegion对象

Result类型的初始值设定项失败?

如何在 Vapor 中制作可选的查询过滤器

ClosedRange.Swift 中 Int 的索引?

为什么 XUIView 中的 Binding 看不到更新后的值?

为什么我在我的 Swift iOS 应用程序项目中收到 Xcode 中的错误消息无法识别的 Select 器发送到类?

Swift Random Float Fatal Error:在无限范围内没有均匀分布

快速递归:函数与闭包

SwiftUI:决定键盘重叠的内容

如何确定扩展中的具体类型?

SwiftUI Grid 中的数组旋转

为 UIActivityViewController Swift 设置不同的活动项

从 NSData 对象在 Swift 中创建一个数组

如何在Swift中找出字母是字母数字还是数字

Swift if 语句 - 多个条件用逗号分隔?

使用相机进行人脸检测

如何使用 Swift 枚举作为字典键? (符合 Equatable)

快速延迟加载属性

Swift 中的单元测试致命错误