fatbobman / blogComments

1 stars 0 forks source link

关于 Core Data 并发编程的几点提示 | 肘子的Swift记事本 #125

Open fatbobman opened 2 years ago

fatbobman commented 2 years ago

https://www.fatbobman.com/posts/concurrencyOfCoreData/

Swift 5.5 提供了盼望已久的 async/await 的功能,为多线程开发带来了前所未有的便利。但 Core Data 由于其特有的并发规则,使用不慎容易导致代码陷入不可控状态,因此让不少开发者对在 Core Data 中进行多线程开发产生了望而却步的情绪。本文将对 Core Data 并发编程中几个常见的问题予以提示,以便开发者更好地了解 Core Data 的并发规则,充分享受 Core Data 提供的强大功能。

akring commented 2 years ago

赞美博主,你的 CoreData 和 CloudKit 的系列文章帮我厘清了思路。每当遇到百思不得其解的问题时,来你的博客查找一番总会有很多收获,感恩 🙏

fatbobman commented 2 years ago

十分高兴能够对你有所帮助。🍺

2022年1月21日 下午12:56,Chris Akring @.***> 写道:

赞美博主,你的 CoreData 和 CloudKit 的系列文章帮我厘清了思路。每当遇到百思不得其解的问题时,来你的博客查找一番总会有很多收获,感恩 🙏

— Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/125#issuecomment-1018180296, or unsubscribe https://github.com/notifications/unsubscribe-auth/ANIYIGKVVBKC7BV775JTR2DUXDROXANCNFSM5HMU7A5Q. You are receiving this because you authored the thread.

beader commented 2 years ago

最后一个例子中,context 中修改了 name 和 age,NSBatchUpdate 修改了 age 和 sex,采取策略 NSMergeByPropertyStoreTrumpMergePolicy。 我的理解是,这里因为采取逐属性比对,冲突的属性为 age,store 胜出,结果应该是 name 采用 context 中的指,age 和 sex 采用 store 中的值。 不知道我理解是否有误。

fatbobman commented 2 years ago

上下文数据在保存时,会发现它的快照版本已经与持久化中的数据版本不一样。按照NSMergeByPropertyStoreTrumpMergePolicy 规则进行比对。因为 上下文仅改动了两个属性 name 和 sex ,因此对于上下文来说,仅有这两个属性会出现冲突的情况,且一旦出现冲突将采用上下文版本的属性内容。 而,如果设置成 NSOverwriteMergePolicy , 则 三个属性都将使用上下文版本的属性内容。

逐个比对的意思是并非哪个数据版本新就使用那个,而是说是否保留没有发生冲突的属性

beader commented 2 years ago

@fatbobman 上下文数据在保存时,会发现它的快照版本已经与持久化中的数据版本不一样。按照NSMergeByPropertyStoreTrumpMergePolicy 规则进行比对。因为 上下文仅改动了两个属性 name 和 sex ,因此对于上下文来说,仅有这两个属性会出现冲突的情况,且一旦出现冲突将采用上下文版本的属性内容。 而,如果设置成 NSOverwriteMergePolicy , 则 三个属性都将使用上下文版本的属性内容。

逐个比对的意思是并非哪个数据版本新就使用那个,而是说是否保留没有发生冲突的属性

我做了如下实验。在 CoreData Stack 的设置中:

container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = false
# 这么设置后,background context 的数据持久化之后,viewContext 不会自动 merge,此时 viewContext 再次 save 时,则会产生冲突

Step1: 在 viewContext 下生成一个 Person(name="Xi", age=10, sex="male"),保存,并存放在 people 中

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Person.name, ascending: true)],
    animation: .default)
private var people: FetchedResults<Person>

...

private func addPerson() {
    withAnimation {
        let newPerson = Person(context: viewContext)
        newPerson.name = "Xi"
        newPerson.age = 10
        newPerson.sex = "male"
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
}

Step 2: 在 background context 中修改该 Person

Button("Change Age & Sex in Background Context") {
    guard let objectId = people.first?.objectID else { return }
    let container = persistenceController.container
    container.performBackgroundTask { bgContext in
        Task {
            await bgContext.perform {                                
                if let person = bgContext.object(with: objectId) as? Person {
                    person.age = 111
                    person.sex = "female"
                    try? bgContext.save()
                }
            }
        }
    }
}

此时:

Step 3: 在 ViewContext 中修改 name 和 age

Button("Change Age & Sex in ViewContext") {
    guard let person = people.first else { return }
    person.name = "XiXi"
    person.age = 222
    try? viewContext.save()
}

save 之后,viewContext 中的对象为 Person(name="XiXi", age=111, sex="female")

name 采用了上下文中的值,age 和 sex 采用了store 中的值。


之后重新阅读了文章最后一段,发现肘子的操作步骤为:

  1. 上下文中修改了 name 和 age
  2. NSBatchUpdaterequest 中修改了 age 和 sex

其实在执行第一步的时候,name 和 age 已经持久化了,变成 store 里面的值了。 NSBatchUpdaterequest 因为不涉及到 context 而直接修改 store,应该不会才生冲突才对。

因此我感觉最后一句话的表述可能不太准确。

fatbobman commented 2 years ago

对不起,我才发现,文章中例子的合并规则标注错误。

我一直以为写的是 NSMergeByPropertyObjectTrumpMergePolicy ,结果文章中的代码竟然写成了 NSMergeByPropertyStoreTrumpMergePolicy 。

是的,你现在的测试结果对于 NSMergeByPropertyStoreTrumpMergePolicy 是完全正确的。

感谢指出错误,避免给大家造成更大的困扰。

文章中已经改回 NSMergeByPropertyObjectTrumpMergePolicy

2022年5月25日 11:48,beader @.***> 写道:

@fatbobman https://github.com/fatbobman 上下文数据在保存时,会发现它的快照版本已经与持久化中的数据版本不一样。按照NSMergeByPropertyStoreTrumpMergePolicy 规则进行比对。因为 上下文仅改动了两个属性 name 和 sex ,因此对于上下文来说,仅有这两个属性会出现冲突的情况,且一旦出现冲突将采用上下文版本的属性内容。 而,如果设置成 NSOverwriteMergePolicy , 则 三个属性都将使用上下文版本的属性内容。

逐个比对的意思是并非哪个数据版本新就使用那个,而是说是否保留没有发生冲突的属性

我做了如下实验。在 CoreData Stack 的设置中:

container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy container.viewContext.automaticallyMergesChangesFromParent = false

这么设置后,background context 的数据持久化之后,viewContext 不会自动 merge,此时 viewContext 再次 save 时,则会产生冲突

Step1: 在 viewContext 下生成一个 Person(name="Xi", age=10, sex="male"),保存,并存放在 people 中

@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Person.name, ascending: true)], animation: .default) private var people: FetchedResults

...

private func addPerson() { withAnimation { let newPerson = Person(context: viewContext) newPerson.name = "Xi" newPerson.age = 10 newPerson.sex = "male" do { try viewContext.save() } catch { let nsError = error as NSError fatalError("Unresolved error (nsError), (nsError.userInfo)") } } } Step 2: 在 background context 中修改该 Person

Button("Change Age & Sex in Background Context") { guard let objectId = people.first?.objectID else { return } let container = persistenceController.container container.performBackgroundTask { bgContext in Task { await bgContext.perform {
if let person = bgContext.object(with: objectId) as? Person { person.age = 111 person.sex = "female" try? bgContext.save() } } } } } 此时:

数据库中为 Person(name="Xi", age=111, sex="female") viewContext 中依然是 Person(name="Xi", age=10, sex="male") Step 3: 在 ViewContext 中修改 name 和 age

Button("Change Age & Sex in ViewContext") { guard let person = people.first else { return } person.name = "XiXi" person.age = 222 try? viewContext.save() } save 之后,viewContext 中的对象为 Person(name="XiXi", age=111, sex="female")

name 采用了上下文中的值,age 和 sex 采用了store 中的值。

之后重新阅读了文章最后一段,发现肘子的操作步骤为:

上下文中修改了 name 和 age NSBatchUpdaterequest 中修改了 age 和 sex 其实在执行第一步的时候,name 和 age 已经持久化了,变成 store 里面的值了。 NSBatchUpdaterequest 因为不涉及到 context 而直接修改 store,应该不会才生冲突才对。

因此我感觉最后一句话的表述可能不太准确。

— Reply to this email directly, view it on GitHub https://github.com/fatbobman/blogComments/issues/125#issuecomment-1136694130, or unsubscribe https://github.com/notifications/unsubscribe-auth/ANIYIGNV3JXSPFSERKT645TVLWPJXANCNFSM5HMU7A5Q. You are receiving this because you were mentioned.

pomeloL commented 1 year ago

你好 谢谢你的文章给我帮助很大 问一个小白问题 是否所有Core Data操作都需要在子线程进行 一些很简单的保存更新是否可以在主线程进行

fatbobman commented 1 year ago

你好 谢谢你的文章给我帮助很大 问一个小白问题 是否所有Core Data操作都需要在子线程进行 一些很简单的保存更新是否可以在主线程进行

当前的 Core Data 已经支持了上下文数据的自动合并,因此使用私有上下文比以前的工作量小些。 事实上,几乎所有的操作都可以在视图上下文( 主线程 )中进行。使用私有上下文最大的作用是避免对主线程造成更大的负担。

beader commented 1 year ago

你好 谢谢你的文章给我帮助很大 问一个小白问题 是否所有Core Data操作都需要在子线程进行 一些很简单的保存更新是否可以在主线程进行

当前的 Core Data 已经支持了上下文数据的自动合并,因此使用私有上下文比以前的工作量小些。 事实上,几乎所有的操作都可以在视图上下文( 主线程 )中进行。使用私有上下文最大的作用是避免对主线程造成更大的负担。

background context还有个应用场景是在编辑页面中,对NSManagedObject进行比较大的改动,譬如增加 relationship 之类的,且希望当用户点击保存时,才真的进行持久化操作。如果用户点了取消,则直接把临时创建的context丢弃即可。直接在 viewContext 操作的话,则需要再进行一些清理动作。

pomeloL commented 1 year ago

@fatbobman

你好 谢谢你的文章给我帮助很大 问一个小白问题 是否所有Core Data操作都需要在子线程进行 一些很简单的保存更新是否可以在主线程进行

当前的 Core Data 已经支持了上下文数据的自动合并,因此使用私有上下文比以前的工作量小些。 事实上,几乎所有的操作都可以在视图上下文( 主线程 )中进行。使用私有上下文最大的作用是避免对主线程造成更大的负担。

感谢指点,我现在是把一些简单的不耗时的Core Data操作在主线程,耗时的丢子线程

ripperhe commented 9 months ago

感谢博主分享,学到很多。🙏

有一个问题想请教下,通过 NSFetchRequest 查询出来的 NSManagedObject 对象拿到其他队列去使用的时候,比如只用来 get 某个属性(例如有个属性叫 name),但是不对这个对象进行任何 set 操作,这样也会有线程问题吗?

fatbobman commented 9 months ago

感谢博主分享,学到很多。🙏

有一个问题想请教下,通过 NSFetchRequest 查询出来的 NSManagedObject 对象拿到其他队列去使用的时候,比如只用来 get 某个属性(例如有个属性叫 name),但是不对这个对象进行任何 set 操作,这样也会有线程问题吗?

有。只能在该托管对象绑定的上下文所在的线程中进行。 也就是说,在私有上下文中获取的数据,不能在视图中读取它的属性。

ripperhe commented 9 months ago

@fatbobman

感谢博主分享,学到很多。🙏

有一个问题想请教下,通过 NSFetchRequest 查询出来的 NSManagedObject 对象拿到其他队列去使用的时候,比如只用来 get 某个属性(例如有个属性叫 name),但是不对这个对象进行任何 set 操作,这样也会有线程问题吗?

有。只能在该托管对象绑定的上下文所在的线程中进行。 也就是说,在私有上下文中获取的数据,不能在视图中读取它的属性。

好的,感谢博主解答 🫡