To test my Core Data implementation I have enabled the launch argument com.apple.CoreData.ConcurrencyDebug 1
. I am getting breakpoints triggered whenever I access a managed object from within a Task
using Swifts new async APIs.
I use a single context (viewContext
) for fetching and ephemeral background contexts to perform write operations. See some snippets of the important parts at the bottom.
My app functions perfectly and I get no breakpoints triggered except for the scenarios where I access a Core Data managed object from within a Task
.
See an example here
func performReloadForecasts() {
Task {
await reloadForecasts()
}
}
The method reloadForecasts
is too long to post here but it references the users favourite location (Hence the locations
) and uses it to fetch the forecast from an API for that location.
Referencing the users locations in views or view models functions fine.
But from what I can tell, by using a Task
to perform an asynchronous operation, I am performing the task in a whole different thread that is chosen (from a pool?) at run time.
Usually the thread that the Task
is run in is com.apple.root.user-initiated-qos.cooperative (serial)
which makes sense I suppose.
How can I refactor or change either my asynchronous functions (such as reloadForecast
) or my core data stack (detailed below) to perform operations in a manor that abides by the concurrency rules for Core Data?
Can I force a Task to run on a particular thread? Since my main context is the viewContext
it would have to be the main thread, which sort of defeats the purpose of the async Task
.
Can I refactor my core data stack to create some sort of thread safe reference to my managed objects? I have seen suggestions to pass object ID's in and refetch the object inside the target thread but surely there is a more elegant way to abide by the concurrency rules.
Core Data Implementation
Context Setup
container = NSPersistentCloudKitContainer(name: "Model")
context = container.viewContext
context.automaticallyMergesChangesFromParent = true
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
Fetching
@Published private var locations: [LocationModel] = []
private func reloadData() {
context.perform { [context] in
do {
self.locations = try context.fetch(LocationModel.fetchRequest())
} catch {
Logger.error("Failed to reload persistence layer.")
Logger.error(error.localizedDescription)
}
}
}
Performing Write Operations
func perform(_ block: @escaping (NSManagedObjectContext) -> Void) {
do {
let context = container.newBackgroundContext()
try context.performAndWait {
block(context)
try context.save()
}
} catch {
Logger.error(error.localizedDescription)
}
}
CodePudding user response:
I fixed some of these issues by adding the @MainActor
flag to the Task
Task { @MainActor in
await reloadForecasts()
}
However I was still getting breakpoints for certain issues, especially higher order functions like maps or sorts. I ended up adding the @MainActor
wrapper to all my view models.
This fixed all of the weird crashes regarding the higher order functions accessing the core data objects, but I faced new problems. My second core data context used for saving objects was now the cause of concurrency breakpoints being triggered.
This made more sense and was much more debug-able. I had strong references to objects fetched in the main context, used to construct another object in the background context.
Model A <---> Model B
I had Model A
that had relationship to another Model B
. To setup the relationship to Model B
, I used a reference to a Model B
object that had been fetched on the main context. But I was creating the Model A
object in the background thread.
To solve this I used the suggested methods of refetching the required objects by ObjectID in the correct context. (Using a bunch of nice helper methods to make things easier)
Here's a forum post asking a related question about ensuring asynchronous tasks are run on the main thread
https://forums.swift.org/t/best-way-to-run-an-anonymous-function-on-the-main-actor/50083
My understanding of the new swift concurrency models is that when you await
on an async function, the task is run on another thread chosen from a pool and when the task is complete, execution returns to the point (and thread) you used await
.
In this case I have forced the Task to start on the main thread (By using @MainActor), execute its task on a thread from the available pool, and return back to the main thread once its completed.
The swift concurrency explains some of this in detail: https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html