Swift 5.5 offers long-awaited async/await capabilities that bring unprecedented convenience to multithreaded development. However, due to the unique concurrency rules of Core Data, it is easy to cause the code to fall into uncontrollable state when it is used carelessly. Therefore, many developers are discouraged from multi-threaded development in Core Data. This article will give you some tips on some common problems in concurrent programming of Core Data, so that developers can better understand the concurrency rules of Core Data and fully enjoy the powerful features provided by Core Data.
The original post was posted on my blog wwww.fatbobman.com
Welcome to subscribe my public account: [Elbow’s Swift Notepad]
Enable concurrent debugging parameters for Core Data
Developers using concurrent programming in Core Data can easily run into situations where the program has no problems during debugging. After the program goes online, due to the increase of users, unexpected, difficult to reproduce, and difficult to locate program exceptions or crashes will occur. A lot of this is due to bad concurrent programming with Core Data.
In order to cause problems for the Core Data concurrency violation to kill during the development phase, in the use of the Core Data framework, be sure to add on launch parameters – com. Apple. CoreData. ConcurrencyDebug 1. This flag forces the program to throw an error immediately upon execution of Core Data code that could theoretically cause concurrent exceptions. Do timely detection, as soon as possible to solve.
The code snippet below will only throw an error if the flag is turned on, otherwise there is a 90% or more chance that the exception will not behave (still a hidden danger).
Use background context to reduce main thread blocking
No matter how fast hardware evolves, operating systems, API frameworks, and services will always try to squeeze it dry. Especially with the increasing refresh rate of the device display, the main thread (UI thread) is under increasing pressure. Reduce Core Data’s footprint on the main thread by creating a background managed object context (private queue context).
In Core Data, we can create two types of managed object context (NS-Managed ObjectContext) — primary queue context and private queue context.
-
The home side column context (NSManagedObjectContextConcurrencyType mainQueueConcurrencyType)
A managed object context defined for and only used on the main queue. Do work related to the interface (UI), mainly to retrieve data from persistent storage for UI display. When using NSPersistentContainer to create a Core Data Stack, the Container’s viewContext property corresponds to the main queue context.
-
Private queues context (NSManagedObjectContextConcurrencyType privateQueueConcurrencyType)
As the name implies, a private queue context creates its own queue when it is created and can only be used on queues it creates itself. This applies to operations that take a long time to execute and may affect the UI response if they are run in the main queue.
There are two ways to create a private queue:
let backgroundContext = persistentContainer.newBackgroundContext() / / way
persistentContainer.performBackgroundTask{ bgContext in 2 / / way
.
}
Copy the code
If the operation has a long lifetime and is frequent, the first approach isto create a private queue dedicated to the transaction (such as Persistent History Tracking).
If this operation is performed infrequently, you can use method 2 to create a private queue temporarily and discard it at will (for example, file import).
Data manipulation through the context of different queues is the most common Core Data concurrency scenario.
Managed object contexts and managed objects are queue-bound
Core Data is designed for multithreaded development. However, not all objects under the Core Data framework are thread-safe. Of these, the managed object context (NS-ManagedoBJectContext) and managed object (NS-Managed Object) that developers touch the most and use the most happen to not be thread-safe.
So when you do concurrent programming in Core Data, make sure you follow these rules:
- The managed object context is bound to the thread (queue) associated with it at initialization.
- Managed objects retrieved from the managed object context are bound to the queue of the owning context.
In layman’s terms, a context is only safe to execute on the queue to which it is bound, as is a managed object.
Create a Core Data template using Xcode, add code to contextView. swift, and turn on the Core Data concurrent debug flag.
The following code will immediately throw an error when executed:
Button("context in wrong queue") {
Task.detached { // Push it to another thread (not the main thread)
print(Thread.isMainThread) // false is not currently on the main thread
let context = PersistenceController.shared.container.viewContext
context.reset() // Most operations that call methods in the main queue context on a non-main thread will fail}}Copy the code
When a viewContext method is called on a non-main thread, the program crashes immediately.
Button("NSManagedObject in wrong context") {// The view runs on the main thread
let backgroundContext = PersistenceController.shared.container.newBackgroundContext() // create a private queue
// An operation was performed on the main thread that should have been performed on the private thread
let item = Item(context: backgroundContext) Item is created in a private context and is bound to a private queue
item.timestamp = .now // Assign a value to the main queue
}
Copy the code
If the Core Data concurrent debugging flag is not turned on, the code above will work fine most of the time, which is why such errors are hard to find.
Use Perform to ensure the correct queue
To eliminate errors in the code above, we must put operations on the managed object context and managed object into the correct queue.
For the main queue context, since the queue it is in is explicit and fixed — the main queue, as long as the operation can be guaranteed to take place in the main queue, it is ok. Such as:
Button("context in wrong queue") {
print(Thread.isMainThread) // true view queue main queue
let context = PersistenceController.shared.container.viewContext
context.reset() There is no problem manipulating the main thread context on the main thread
}
Copy the code
Or you can make sure the operation is on the main thread by using dispatchqueue.main.async or mainactor.run.
However, for a private context, because the queue is private and exists only inside the NSManagedObjectContext instance, it can only be called through the Perform or performAndwait methods. The difference between perform and performAndwait is how a given block of code executes, asynchronously or synchronously.
Starting with iOS 15 (macOS Monterey), Core Data provides async/await versions of the above methods. Combine the two and set the task type with the schedule parameter. Immediate Immediate plan task, enqueued Queued plan task.
perform<T>(schedule: NSManagedObjectContext.ScheduledTaskType = .immediate, _ block: @escaping(a)throws -> T) async rethrows -> T
Copy the code
Execute the code that caused the crash in Perform to eliminate the error.
Button("context in wrong queue") {
/ / the home team
Task.detached { // Push to another queue (not the main queue)
print(Thread.isMainThread) // false
let context = PersistenceController.shared.container.viewContext
await context.perform { // Adjust back to the context queue (in this case, the main queue)
context.reset()
}
}
}
Button("NSManagedObject in wrong context") {// View main thread
let backgroundContext = PersistenceController.shared.container.newBackgroundContext() // create a private queue
backgroundContext.perform { // Execute in the private queue of the backgroundContext
let item = Item(context: backgroundContext)
item.timestamp = .now
}
}
Copy the code
Unless the developer is absolutely sure that the code is running in the main queue and is calling the main queue context or a managed object belonging to that context, it is safest to use Perform to prevent errors.
Find the context by nS-managed object
In some cases, only the managed object (NS-managed Object) can be obtained, and the context of the managed object can be obtained from it to ensure that it is operated on in the correct queue.
Such as:
/ / Item for NSManagedObject
func delItem(item:Item) {
guard let context = item.managedObjectContext else {return}
context.perform {
context.delete(item)
try! context.save()
}
}
Copy the code
The context for the managed object is declared as unowned(unsafe). Use this if verifying that context still exists.
We use NS-managed BjECTId
Because the managed object is bound to the same queue as the context that hosts it, there is no way to pass an NSManageObject between the contexts of different queues.
In cases where you need to operate on the same data record in different queues, the solution is to use NS-managed Bjectid.
Delete Item code above, for example: suppose the managed object is get in the home side columns (in the view through the @ FetchRequest or NSFetchedResultsController), click the view button, call delItem. To relieve stress on the main thread, delete data on the private queue.
Adjusted code:
func delItem(id:NSManagedObjectID) {
let bgContext = PersistenceController.shared.container.newBackgroundContext()
bgContext.perform {
let item = bgContext.object(with: id)
bgContext.delete(item)
try! bgContext.save()
}
}
Copy the code
Or you can still take NS-managed object as an argument
func delItem(item:Item) {
let id = item.objectID
let bgContext = PersistenceController.shared.container.newBackgroundContext()
bgContext.perform {
let item = bgContext.object(with: id)
bgContext.delete(item)
try! bgContext.save()
}
}
Copy the code
Careful readers may wonder if managed objects cannot be called on other queues. Wouldn’t there be a problem getting an objectID or managedObjectContext from a managed object? In fact, although the vast majority of managed object contexts and managed object properties and methods are non-thread-safe, there are a few properties that can be used safely on other threads. For example, manage object objectID, managedObjectContext, hasChanges, and isFault. The managed object context persistentStoreCoordinator, automaticallyMergesChangesFromParent, etc.
As a compact generic identifier for managed objects, NS-managedobJectid is widely used in the Core Data framework. For example, nS-managed BJectid is used for batch operations, persistent history tracking, context notifications, and so on. But it’s important to note that it’s not immutable. For example, if a managed object is created and not persisted, it will first generate a temporary ID, which will be persisted and then converted back to a persistent ID. Or it may change if the version of the database or some meta information changes (Apple does not disclose its generation rules).
Do not use this as a unique identifier for the managed object (like the existence of a primary key) except when the program is running, and it is best to do this by creating your own ID attribute (such as UUID).
If you really need to archive ids, you can convert NS-managed Bjectid to URI representation. For a specific use case, see Displaying Core Data in your application in Spotlight
The previous example used object(with: ID) to get managed objects. Other contextual methods to get managed objects via NSSManagedoBJectid include regiesterdObject and existingObject. Their applications vary, as shown in the table below.
Merge changes between different contexts
Using the delItem code above, managed objects in the main thread context still exist after they are removed in the background context. This data does not change if it is displayed on the screen. Only to a context (in this case the background context) as the change of merged into another context (master below), the changes will be reflected in the interface (@ FetchRequest or NSFetchedResultsController).
Prior to iOS 10, merging context changes required the following steps:
- Add an observer to monitor the Core Data context saved notice sending (Notification. Name. NSManagedObjectContextDidSave)
- In the observer, the userInfo of the notification and the context to merge are passed as parameters to mergeChanges
NotificationCenter.default.addObserver(forName:Notification.Name.NSManagedObjectContextDidSave, object: nil, queue: nil, using: merge)
func merge(_ notification:Notification) {
let userInfo = notification.userInfo ?? [:]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [container.viewContext])
}
Copy the code
You can also merge context by context using the mergeChanges method of the NS-managed BjectContext instance.
In the iOS version 10, the Core Data for NSManagedObjectContext added automaticallyMergesChangesFromParent properties.
Context automaticallyMergesChangesFromParent attribute set to true, then the context changes will be automatically merged other context changes. In the Core Data with CloudKit synchronize the local database (2) – ripped from private database, you can see how the automaticallyMergesChangesFromParent will be reflected the change of network Data in the user interface.
Set the correct merge policy
When multiple contexts or persistent storage coordinators are used, conflicts can occur when holding managed objects in different environments.
The merge in the merge strategy in this section does not refer to the context merge in the previous section. A merge policy setting to resolve save conflicts caused by inconsistent versions of optimistic locks for managed objects when they are persisted.
Although concurrency is not a requirement for save conflicts, save conflicts are very easy to occur in a concurrent environment.
Here’s an example to give you an intuitive understanding of save conflicts:
- The managed object A (corresponding to data B in the database) was obtained from the database using fetch below.
- Modified data B in the database using NSBatchUpdate Equest (without context).
- Modify managed object A on main below, try to save.
- At save time, the optimistic lock version number of A is inconsistent with the new version number of database B, resulting in A save conflict. At this point, it is necessary to set the merger strategy to solve the problem of how to choose.
Use mergePolicy to set merge conflict policies. If this property is not set, Core Data defaults to using NSErrorMergePolicy as a conflict resolution policy (all conflicts are not handled and an error is reported), which results in Data being improperly saved to the local database.
viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
Copy the code
Core Data preset four merge conflict policies, which are:
- NSMergeByPropertyStoreTrumpMergePolicy
On a property-by-property comparison, if persistent data and in-memory data both change and conflict, persistent data wins
- NSMergeByPropertyObjectTrumpMergePolicy
On a property-by-property basis, if persistent data and in-memory data both change and conflict, in-memory data wins
- NSOverwriteMergePolicy
In-memory data always wins
- NSRollbackMergePolicy
Persistent data always wins
If the default merge policy does not meet your needs, you can also create a custom merge policy by inheriting NSMergePolicy.
Use the example above to introduce the strategy:
- Data B has three attributes: name, age and sex
- Name and age are modified in the context
- NSBatchUpdaterequest changes age and sex
- The current setting of the merger strategy for NSMergeByPropertyStoreTrumpMergePolicy
- The final merge results in name and age being modified by context, and sex keeping the NSBatchUpdaterequest changes.
conclusion
Core Data has a set of rules that developers should follow, and if you break them, Core Data will teach you a hard lesson. But once the rules are mastered, what was once a barrier will no longer be a problem.
I hope this article has been helpful to you.
The original post was posted on my blog wwww.fatbobman.com
Welcome to subscribe my public account: [Elbow’s Swift Notepad]