Reputation: 45
I run into the problem of core data concurrency when retrieving a NSManagedObj from core data when the thread is not main thread.
I played around with core data of Swift 4 and everything worked well initially when the codes were simple. However, when I added more codes for core data and increased the the complexity, I ran into an error by occasion and discovered the concurrency issue of core data.
The error that I ran into was
CoreData: error: NULL _cd_rawData but the object is not being turned into a fault
I searched some ways trying to solve the issue by adding the below line to handle the retrieving core data action when it comes to the background thread instead of the main thread.
appDelegate.persistentContainer.performBackgroundTask{....}
However, it still has problems.
First, here is the retrieve function
func retrieve() -> MasterSlave?{
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
var count = 0
var masterSlave: MasterSlave?
DispatchQueue.main.async {
print("master slave receive main thread is", Thread.isMainThread)
count = appDelegate.retrieve("MasterSlave", predicate: nil, sort: nil, limit: nil)!.count
if count > 0 {
masterSlave = appDelegate.retrieve("MasterSlave", predicate: nil, sort: nil, limit: nil)?.first! as? MasterSlave
}
}
appDelegate.persistentContainer.performBackgroundTask{ context in
print("master slave receive main thread is", Thread.isMainThread)
let ms = appDelegate.retrieve("MasterSlave", predicate: nil, sort: nil, limit: nil)!
print("ms obj retreived is", ms, "count is ", ms.count)
count = appDelegate.retrieve("MasterSlave", predicate: nil, sort: nil, limit: nil)!.count
if count > 0 {
masterSlave = appDelegate.retrieve("MasterSlave", predicate: nil, sort: nil, limit: nil)?.first! as? MasterSlave
}
}
return masterSlave
}
And here is the retrieve function in appDelegate
func retrieve(_ myEntityName:String, predicate:String?, sort:[[String:Bool]]?, limit:Int?) -> [NSManagedObject]? {
let myContext = persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: myEntityName)
// predicate
if let myPredicate = predicate {
request.predicate = NSPredicate(format: myPredicate)
}
// sort
if let mySort = sort {
var sortArr :[NSSortDescriptor] = []
for sortCond in mySort {
for (k, v) in sortCond {
sortArr.append(NSSortDescriptor(key: k, ascending: v))
}
}
request.sortDescriptors = sortArr
}
// limit
if let limitNumber = limit {
request.fetchLimit = limitNumber
}
do {
return try myContext.fetch(request) as? [NSManagedObject]
} catch {
fatalError("\(error)")
}
return nil
}
Now, something weird is found:
The MasterSlave entity should have only one object which I saved once before. So when it runs let ms = appDelegate.retrieve("MasterSlave", predicate: nil, sort: nil, limit: nil)!
, it is expected to retrieve the following data
data: {
encryptKey = nil;
hasMaster = 0;
lock = 0;
master = 0;
password = nil;
slave = 1;
}
But it gives the following instead
[<MasterSlave: 0x600000280190> (entity: MasterSlave; id: 0xd000000000040004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p1> ; data: <fault>),
<MasterSlave: 0x600000280140> (entity: MasterSlave; id: 0xd000000000080004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p2> ; data: <fault>),
<MasterSlave: 0x60000009fd60> (entity: MasterSlave; id: 0xd0000000000c0004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p3> ; data: <fault>),
<MasterSlave: 0x6000002800f0> (entity: MasterSlave; id: 0xd000000000100004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p4> ; data: <fault>),
<MasterSlave: 0x6000002800a0> (entity: MasterSlave; id: 0xd000000000140004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p5> ; data: <fault>),
<MasterSlave: 0x600000280050> (entity: MasterSlave; id: 0xd000000000180004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p6> ; data: <fault>),
<MasterSlave: 0x600000280000> (entity: MasterSlave; id: 0xd0000000001c0004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p7> ; data: <fault>),
<MasterSlave: 0x60000009ff90> (entity: MasterSlave; id: 0xd000000000200004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p8> ; data: <fault>),
<MasterSlave: 0x60000009ff40> (entity: MasterSlave; id: 0xd000000000240004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p9> ; data: <fault>),
<MasterSlave: 0x60000009fef0> (entity: MasterSlave; id: 0xd000000000280004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p10> ; data: <fault>),
<MasterSlave: 0x60000009fea0> (entity: MasterSlave; id: 0xd0000000002c0004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p11> ; data: <fault>),
<MasterSlave: 0x60000009fe50> (entity: MasterSlave; id: 0xd000000000300004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p12> ; data: <fault>),
<MasterSlave: 0x60000009fe00> (entity: MasterSlave; id: 0xd000000000340004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p13> ; data: <fault>),
<MasterSlave: 0x60000009fdb0> (entity: MasterSlave; id: 0xd000000000380004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p14> ; data: <fault>),
<MasterSlave: 0x60000009fcc0> (entity: MasterSlave; id: 0xd0000000003c0004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p15> ; data: <fault>),
<MasterSlave: 0x600000280230> (entity: MasterSlave; id: 0xd000000000400004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p16> ; data: <fault>),
<MasterSlave: 0x60000009fa90> (entity: MasterSlave; id: 0xd000000000440004 <x-coredata://5FF8490C-B1C5-417D-BA3E-2F30B5D2BAD6/MasterSlave/p17> ; data: {
encryptKey = nil;
hasMaster = 0;
lock = 0;
master = 0;
password = nil;
slave = 1;
})]
When I stop the app and deploy again, the count of the [NSManagedObject] of masterSlave become 18 instead of 17.
I don't understand why the count of the retrieved [NSManagedObject] increments each time the deployment is built. Also, when it is in main thread, things work properly.
Does anyone have any ideas?
Upvotes: 0
Views: 749
Reputation: 8563
Core-data is not thread safe. Neither for reading or for writing. Every context has one (and only one) thread that it is safe to access from. For the viewContext it is the main thread. For contexts created by performBackgroundTask
it should only be accessed inside that block. This is true both for the context and all managedObjects associated with the context.
Your retrieve
method, where you fetch from core-data, you should accept a context as a parameter. When it is used from the main they it should be passed the viewContext; when it is from a performBackgroundTask it should use the context that was passed to the block.
You cannot pass objects in to or out of a performBackgroundTask block. So you should not access any main-thread object from inside a performBackgroundTask. Instead you should save the object's objectID and refetch using it. You cannot pass managedObjects outside of the block, but you pass the data that inside of them.
Your use of blocks in retrieve()
is nonsensical. The method returns before either block is executed and masterSlave
will alway be nil. Generally, you should fetch on the main thread from a method that you know is already on the main thread. Using DispatchQueue.main.async
to make a fetch and then try to send that data back to a background thread is never a good idea - you generally want the information on the main thread where you can display the information.
If you want to guarantee that there will be only one instance of an entity you should do a fetch for that object before you create it. It looks like you are creating a new object every time the app launches. You have not shared that code so I cannot comment further.
If you expect to have less than around a hundred entities reading a writing only on the viewContext is not an unreasonable setup. If that is the case I would recommend doing so as it would fix most of your issues.
Upvotes: 1