Introduction
Core Data's NSManagedObjectContext is not thread-safe. Each context is bound to a specific dispatch queue, and accessing managed objects from the wrong queue triggers a concurrency violation. In debug mode, this logs a warning. In production, it causes unpredictable crashes, data corruption, and lost saves.
Symptoms
- Console warning:
CoreData: warning: Multithreading CONVIOLATION NSManagedObjectContextaccessed from multiple threads- Random crashes during
context.save() - Data appears corrupted or duplicated after save
- Fault firing fails with
NSObjectInaccessibleException
Example warning:
``
CoreData: warning: A NSManagedObjectContext was used from a thread other
than the one it was created on. This is not supported and will result in
data corruption. Context: <NSManagedObjectContext: 0x600001234567>,
thread: <NSThread: 0x600000111111>,
created on thread: <NSThread: 0x600000222222>
Common Causes
- Passing
NSManagedObjectinstances between threads instead ofNSManagedObjectID - Accessing context properties from a background callback without
perform - Using
NSMainQueueConcurrencyTypecontext from a background queue - SwiftUI
@FetchRequesttriggering access on background thread - Completion handlers executing on arbitrary GCD queues
Step-by-Step Fix
- 1.Pass object IDs between threads, not managed objects:
- 2.```swift
- 3.// WRONG: passing the object itself
- 4.func processOnBackground(user: User) {
- 5.DispatchQueue.global().async {
- 6.// user was created on main queue - CONVIOLATION!
- 7.print(user.name)
- 8.}
- 9.}
// CORRECT: pass the object ID func processOnBackground(userID: NSManagedObjectID) { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.perform { guard let user = try? backgroundContext.existingObject(with: userID) as? User else { return } print(user.name) // Safe: user was fetched on this context's queue } } ```
- 1.Use perform for all context access:
- 2.```swift
- 3.let context = persistentContainer.newBackgroundContext()
- 4.context.perform {
- 5.// All Core Data operations happen inside perform block
- 6.let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
- 7.let users = try? context.fetch(fetchRequest)
for user in users ?? [] { user.lastSyncDate = Date() }
try? context.save() } ```
- 1.Update UI from main queue context:
- 2.```swift
- 3.// Background context does the work
- 4.let backgroundContext = persistentContainer.newBackgroundContext()
- 5.backgroundContext.perform {
- 6.// Fetch and process on background
- 7.let items = try? backgroundContext.fetch(request)
// Save changes try? backgroundContext.save()
// Update UI on main queue DispatchQueue.main.async { // Use main context for UI self.mainContext.perform { let refreshedItems = try? self.mainContext.fetch(request) self.tableView.reloadData() } } } ```
- 1.Use parent-child context hierarchy:
- 2.```swift
- 3.func createBackgroundContext() -> NSManagedObjectContext {
- 4.let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
- 5.context.parent = persistentContainer.viewContext // View context is parent
- 6.context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
- 7.return context
- 8.}
// Save propagates to parent backgroundContext.perform { // Make changes try? backgroundContext.save() // Pushes to parent (viewContext)
// Save parent to persist to store self.persistentContainer.viewContext.perform { try? self.persistentContainer.viewContext.save() } } ```
Prevention
- Set
context.shouldDeleteInaccessibleFaults = trueto catch issues early - Enable
-com.apple.CoreData.ConcurrencyDebug 1in launch arguments during development - Always use
performorperformAndWaitfor context access - Never store
NSManagedObjectreferences across thread boundaries - Use
NSPersistentContainer.newBackgroundContext()for background work - In SwiftUI, use
@FetchRequestwhich automatically uses the view context on the main queue