简体   繁体   中英

How does Core data concurrency work with multithreading in Swift 3?

In my program, it uses both of

DispatchQueue.global(qos: .background)

and

self.concurrentQueue.sync(flags: .barrier)

to deal with the background multithread issues.

It is swift 3 so I use the latest way to get the childContext:

lazy var context: NSManagedObjectContext = {
    return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.newBackgroundContext()
}()

I also enable -com.apple.CoreData.ConcurrencyDebug 1 to debug

Then the problem occurs:

1, When there's an API call and in the callback block (background thread), I need to fetch the core data, edit, then save. I tried to use self.context from the code above to call performBlockAndWait and do save inside of this block. The whole process goes fine but when I try to access my result outside of this block but inside of the callback block, the error occurs. I have also tried to get the objectId and getObjectById by both self.context and self.context.parent and the error occurs on this line. What did I do wrong and how should I do this? since I need to use the result everywhere in many different thread (not context).

2, I read a post says that I need one context per thread, then in my case, how do I determine which exact thread it is if it's a call back from API call and do I really need to do this?

3, You might ask that why do I need a privateConcurrentType, because my program has things need to be running in background thread so that it has to do it this way, (read from other post), is this right?

4, Even in my question 1, get object by passing objectId to different Context still not working in my case. Let's assume this is the proper way. How am I gonna manage passing so many objectID throughout my entire program in different thread without being super messy? To me this sounds crazy but I suppose there's a much cleaner and easier way to deal with this.

5, I have read many posts some are pretty old (before swift 3), they have to do childContext.save then parentContext.save , but since I use the code above (swift 3 only). It seems that I can do childContext.save only to make it work? Am I right?

Core data in general is not multithreading friendly. To use it on concurrent thread I can assume only bad things will happen. You may not simply manipulate managed objects outside the thread on which the context is.

As you already mentioned you need a separate context per thread which will work in most cases but by my experience you only need one background context which is read-write and a single main thread read-only context that is used for fetch result controllers or other instant fetches.

Think of a context as some in-memory module that communicates with the database (a file). Fetched entities are shared within the context but are not shared between contexts. So you can modify pretty much anything inside the context but that will not show in the database or other contexts until you save the context into the database. And if you modify the same entity on 2 contexts and then save them you will get a conflict which should be resolved by you.

All of these then make quite a mess in the code logic and so multiple contexts seem like something to avoid. What I do is create a background context and then do all of the operations on that context. Context has a method perform which will execute the code on its own thread which is not main (for background context) and this thread is serial.

So for instance when doing a smart client I will get a response from server with new entries. These are parsed on the fly and I perform a block on context to get all the corresponding objects in the database and create the ones that do not exist. Then copy the data and save the context into database.

For the UI part I do similar. Once an entry should be saved I either create or update the entity on the background context thread. Then usually do some UI stuff on completion so I have a method:

    public func performBlockOnBackgroundContextAndReturnOnMain(block: @escaping (() -> Void), main: @escaping (() -> Void)) {
        if let context = context {
            context.perform {
                block()
                DispatchQueue.main.async(execute: { () -> Void in
                    main()
                })
            }
        }
    }

So pretty much all of the core data logic happens on a single thread which is in background. For some cases I do use a main context to get items from fetch result controller for instance; I display a list of objects with it and once user selects one of the items I refetch that item from the background context and use that one in the user interface and to modify it.

But even that may give you trouble as some properties may be loaded lazily from database so you must ensure that all the data you need will be loaded on the context and you may access them on the main thread. There is method for that but I rather use wrappers:

I have a single superclass for all the entities in the database model which include id only. So I also have a superclass wrapper which has all the logic to work with the rest of wrappers. What I am left with in the end is that for each of the subclass I need to override 2 mapping methods (from and to) managed object.

It might seem silly to create additional wrappers and to copy the data into memory from managed object but the thing is you need to do that for most of the managed objects anyway; Converting NSData to/from UIImage , NSDate to/from Date , enumerations to/from integers or strings... So in the end you are more or less just left with strings that are copied 1-to-1. Also this makes it easy to have the code that maps the response from your server in this class or any additional logic where you will have no naming conflicts with managed objects.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM