Comprensión durante la lectura
¿Cuándo tiene sentido usar un actor personalizado versus el viewContext directamente?
actor CoreDataStore {
static let shared = CoreDataStore()
let persistentContainer: NSPersistentContainer
nonisolated let modelExecutor: NSModelObjectContextExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
modelExecutor.asUnownedSerialExecutor()
}
private var context: NSManagedObjectContext { modelExecutor.context }
private init() {
persistentContainer = NSPersistentContainer(name: "CoreDataConcurrency")
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
modelExecutor = NSModelObjectContextExecutor(context: persistentContainer.newBackgroundContext())
Task {
do {
try await persistentContainer.loadPersistentStores()
} catch {
print("Failed to load persistent store: \(error)")
}
}
}
func deleteAllAndSave<T: NSManagedObject>(using fetchRequest: NSFetchRequest<T>) throws {
let objects = try context.fetch(fetchRequest)
for object in objects {
context.delete(object)
}
try context.save()
}
}
extension NSPersistentContainer {
func loadPersistentStores() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
self.loadPersistentStores { storeDescription, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
}
}
}
}
La mayoría de aplicaciones que usan Core Data trabajan directamente con viewContext, así que introducir el actor CoreDataStore va a forzar usar concurrencia en lugares donde no es realmente necesaria (porque se puede usar viewContext desde @MainActor directamente).
Se podría decir que tal vez en segundo plano se puede hacer, sin embargo, puede ser más fácil usar:
try await backgroundContext.perform {
/// Perform heavy query & work...
try backgroundContext.save()
}
¿Qué diferencia hay entre un actor regular y un global actor para este caso?
@globalActor
actor CoreDataBackgroundContext {
static let shared = CoreDataBackgroundContext()
nonisolated let modelExecutor: NSModelObjectContextExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
modelExecutor.asUnownedSerialExecutor()
}
init() {
let backgroundContext = CoreDataStore.shared.persistentContainer.newBackgroundContext()
modelExecutor = NSModelObjectContextExecutor(context: backgroundContext)
}
}
nonisolated struct CoreDataStoreStructure {
let viewContext: NSManagedObjectContext
let backgroundContext: NSManagedObjectContext
@CoreDataBackgroundContext
func performHeavyQuery(closure: (NSManagedObjectContext) throws -> Void) rethrows {
try closure(backgroundContext)
}
@MainActor
func performWorkOnViewContext(closure: @escaping (NSManagedObjectContext) throws -> Void) async rethrows {
try closure(viewContext)
}
}
Usar el actor normal podría obligar a que todos los llamados usen concurrencia. Sin embargo, al usar el actor global, se podría crear un par de métodos (performHeavyQuery y performWorkOnViewContext) para envolver el llamado a los contextos respectivos, en lugar de forzar al desarrollar a hacer await siempre. Sin embargo, esta implementación depende de que el desarrollar llame correctamente el método que es (por ejemplo: llamar el método performWorkOnViewContext desde un hilo de segundo plano).
¿Qué rol cumple SerialExecutor en la implementación de NSModelObjectContextExecutor?
SerialExecutor me permite serializar el acceso al NSManagedObjectContext dentro de NSModelObjectContextExecutor. Notar que dentro de enqueue se ejecuta context.perform, lo que implica que cada tarea se va a ejecutar de forma serial.
import CoreData
final class NSModelObjectContextExecutor: @unchecked Sendable, SerialExecutor {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let unownedExecutor = asUnownedSerialExecutor()
/// Execute the enqueued job on the configured managed object context.
context.perform {
unownedJob.runSynchronously(on: unownedExecutor)
}
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
¿Por qué se usa @unchecked Sendable en el executor?
El executor va a tener una referencia a NSManagedObjectContext que, a pesar de ser let, no es Sendable. Por esto, es @unchecked Sendable.
¿Qué hace el método enqueue(_:) y por qué es necesario?
enqueue(_:) ejecuta las tareas que hayan sido encoladas en el executor.
Implementación final
nonisolated struct CoreDataStore {
static let shared = CoreDataStore()
let persistentContainer: NSPersistentContainer
private var viewContext: NSManagedObjectContext { persistentContainer.viewContext }
private init() {
persistentContainer = NSPersistentContainer(name: "CoreDataConcurrency")
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
Task { [persistentContainer] in
do {
try await persistentContainer.loadPersistentStores()
} catch {
print("Failed to load persistent store: \(error)")
}
}
}
@MainActor
func perform(_ block: (NSManagedObjectContext) throws -> Void) rethrows {
try block(viewContext)
}
@concurrent func deleteAllAndSave<T: NSManagedObject>(using fetchRequest: NSFetchRequest<T>) async throws {
let backgroundContext = persistentContainer.newBackgroundContext()
try await backgroundContext.perform {
let objects = try backgroundContext.fetch(fetchRequest)
for object in objects {
backgroundContext.delete(object)
}
try backgroundContext.save()
}
}
}
Top comments (0)