Tom
Tom

Reputation: 53

Crashes during CoreData fetching on serial queue

I went through many discutions and subjects about CoreData, but I keep getting the same problem.

Here's the context : I have an application which have to do several access to CoreData. I decided, for simplifying, to declare a serial thread specifically for access (queue.sync for fetching, queue.async for saving). I have a structure that is nested three times, and for recreating the entire structure, I fetch subSubObject, then SubObject and finally Object

But sometimes (like 1/5000 recreation of "Object") CoreData crash on fetching results, with no stack trace, with no crash log, only a EXC_BAD_ACCESS (code 1)

Objects are not in cause, and the crash is weird because all access are done in the same thread which is a serial thread

If anyone can help me, I will be very grateful !

Here's the structure of the code :

private let delegate:AppDelegate
private let context:NSManagedObjectContext
private let queue:DispatchQueue

override init() {
    self.delegate = (UIApplication.shared.delegate as! AppDelegate)
    self.context = self.delegate.persistentContainer.viewContext
    self.queue = DispatchQueue(label: "aLabel", qos: DispatchQoS.utility)
    super.init()
}


(...)

public func loadObject(withID ID: Int)->Object? {

    var object:Object? = nil

    self.queue.sync {

        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
        fetchRequest.predicate = NSPredicate(format: "id == %@", NSNumber(value: ID))

        do {
            var data:[NSManagedObject]

            // CRASH HERE ########################
            try data = context.fetch(fetchRequest)
            // ###################################

            if (data.first != nil) {
                let subObjects:[Object] = loadSubObjects(forID: ID)
                // Task creating "object"
            }
        } catch let error as NSError {
            print("CoreData : \(error), \(error.userInfo)")
        }
    }

    return object
}

private func loadSubObjects(forID ID: Int)->[Object] {

    var objects:[Object] = nil

    self.queue.sync {

        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
        fetchRequest.predicate = NSPredicate(format: "id == %@", NSNumber(value: ID))

        do {
            var data:[NSManagedObject]

            // OR HERE ###########################
            try data = context.fetch(fetchRequest)
            // ###################################

            if (data.first != nil) {
                let subSubObjects:[Object] = loadSubObjects(forID: ID)
                // Task creating "objects"
            }
        } catch let error as NSError {
            print("CoreData : \(error), \(error.userInfo)")
        }
    }

    return objects
}

(etc...)

Upvotes: 5

Views: 3060

Answers (1)

Jon Rose
Jon Rose

Reputation: 8563

TL;DR: get rid of your queue, replace it with an operation queue. Run fetches on the main thread with viewContext do writing in one synchronous way.

There are two issues. First is that managedObjectContexts are not thread safe. You cannot access a context (neither for reading or for writing) except from the single thread that it is setup to work with. The second issue is that you shouldn't be doing multiple writing to core-data at the same time. Simultaneous writes can lead to conflicts and loss of data.

The crash is cause by accessing viewContext from a thread that is not the main thread. The fact that there is a queue ensuring that nothing else is accessing core data at the same time doesn't fix it. When core-data thread safety is violated core-data can fail at any time and in any way. That means that it may crash with hard to diagnose crash reports even at points in the code where you are on the correct thread.

You have the right idea that core-data needs a queue to work well when saving data, but your implementation is flawed. A queue for core-data will prevent write conflicts caused by writing conflicting properties to an entity at the same time from different context. Using NSPersistentContainer this is easy to set up.

In your core-data manager create a NSOperationQueue

let  persistentContainerQueue : OperationQueue = {
    let queue = OperationQueue.init();
    queue.maxConcurrentOperationCount = 1;
    return queue;
}()

And do all writing using this queue:

func enqueueCoreDataBlock(_ block: @escaping (NSManagedObjectContext) -> Swift.Void){
    persistentContainerQueue.addOperation {
        let context = self.persistentContainer.newBackgroundContext();
        context.performAndWait {
            block(context)
            do{
                try context.save();
            } catch{
                //log error
            }
        }
    }
}

For writing use enqueueCoreDataBlock: which will give you a context to use and will execute every block inside the queue so you don't get write conflicts. Make sure that no managedObject leave this block - they are attached to the context which will be destroyed at the end of the block. Also you can't pass managedObjects into this block - if you want to change a viewContext object you have to use the objectID and fetch in the background context. In order for the changes to be seen on the viewContext you have to add to your core-data setup persistentContainer.viewContext.automaticallyMergesChangesFromParent = true

For reading you should use the viewContext from the main thread. As you are generally reading in order to display information to the user you aren't gaining anything by having a different thread. The main thread would have to wait for the information in any event so it is faster just the run the fetch on the main thread. Never write on the viewContext. The viewContext does not use the operation queue so writing on it can create write conflicts. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.

At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.

Upvotes: 9

Related Questions