Threading and Concurrency
Threading and Concurrency
Section titled “Threading and Concurrency”Core Data threading rules are strict but essential for data integrity. This guide covers safe multi-threading patterns, common pitfalls, and debugging techniques.
The Golden Rule
Section titled “The Golden Rule”Never pass NSManagedObject instances between threads. Always use NSManagedObjectID.
// ❌ WRONG: Passing object between contextslet article = viewContext.object(...)backgroundContext.perform { article.name = "Updated" // CRASH!}
// ✅ CORRECT: Pass object IDlet objectID = article.objectIDbackgroundContext.perform { guard let article = try? backgroundContext.existingObject(with: objectID) as? Article else { return } article.name = "Updated" // Safe! try? backgroundContext.save()}Why NSManagedObjectID is Thread-Safe
Section titled “Why NSManagedObjectID is Thread-Safe”NSManagedObjectID is immutable and thread-safe. It’s a unique identifier that works across contexts and threads.
// Object IDs are thread-safelet objectID: NSManagedObjectID = article.objectID
// Can be passed to any thread/contextDispatchQueue.global().async { let context = container.newBackgroundContext() context.perform { if let article = try? context.existingObject(with: objectID) as? Article { // Work with article safely } }}Context Types and Concurrency
Section titled “Context Types and Concurrency”View Context (Main Queue)
Section titled “View Context (Main Queue)”Runs on the main thread. Use for all UI operations.
let viewContext = container.viewContextviewContext.perform { // Runs on main thread let article = Article(context: viewContext) article.name = "New Article" try? viewContext.save()}Characteristics:
- Main queue concurrency type
- Runs on main thread
- Use for UI-related operations only
- Keep operations lightweight
Background Context (Private Queue)
Section titled “Background Context (Private Queue)”Runs on a private queue. Use for heavy work.
let backgroundContext = container.newBackgroundContext()backgroundContext.perform { // Runs on private background queue for i in 0..<1000 { let article = Article(context: backgroundContext) article.name = "Article \(i)" } try? backgroundContext.save()}Characteristics:
- Private queue concurrency type
- Runs on background thread
- Use for imports, exports, batch operations
- Doesn’t block UI
perform vs performAndWait
Section titled “perform vs performAndWait”perform (Asynchronous - Preferred)
Section titled “perform (Asynchronous - Preferred)”context.perform { // Work happens asynchronously let article = Article(context: context) try? context.save()}// Code here runs immediately, before perform block finishesBenefits:
- Non-blocking
- Better performance
- Recommended for most cases
performAndWait (Synchronous - Use Sparingly)
Section titled “performAndWait (Synchronous - Use Sparingly)”context.performAndWait { // Work happens synchronously let article = Article(context: context) try? context.save()}// Code here runs after perform block finishesCaution:
- Blocks the calling thread
- Can block main thread even for background contexts
- Use only when you need the result immediately
Example of blocking behavior:
// Called from main threadlet backgroundContext = container.newBackgroundContext()
// This BLOCKS the main thread!backgroundContext.performAndWait { // Heavy work here blocks UI for i in 0..<10000 { let article = Article(context: backgroundContext) }}Common Threading Patterns
Section titled “Common Threading Patterns”Pattern 1: Background Import
Section titled “Pattern 1: Background Import”func importArticles(_ data: [ArticleData]) { let backgroundContext = container.newBackgroundContext() backgroundContext.perform { for item in data { let article = Article(context: backgroundContext) article.name = item.name article.content = item.content }
do { try backgroundContext.save() } catch { print("Failed to save: \(error)") } }}Pattern 2: Update Object from Background
Section titled “Pattern 2: Update Object from Background”func updateArticle(_ article: Article, newName: String) { let objectID = article.objectID let backgroundContext = container.newBackgroundContext()
backgroundContext.perform { guard let article = try? backgroundContext.existingObject(with: objectID) as? Article else { return }
article.name = newName try? backgroundContext.save() }}Pattern 3: Fetch in Background, Update UI on Main
Section titled “Pattern 3: Fetch in Background, Update UI on Main”func loadArticles(completion: @escaping ([Article]) -> Void) { let backgroundContext = container.newBackgroundContext()
backgroundContext.perform { let fetchRequest = Article.fetchRequest() guard let articles = try? backgroundContext.fetch(fetchRequest) else { return }
// Get object IDs (thread-safe) let objectIDs = articles.map { $0.objectID }
// Switch to main context for UI update DispatchQueue.main.async { let viewContext = self.container.viewContext let mainArticles = objectIDs.compactMap { try? viewContext.existingObject(with: $0) as? Article } completion(mainArticles) } }}Pattern 4: Batch Delete with Object IDs
Section titled “Pattern 4: Batch Delete with Object IDs”func deleteArticles(_ articles: [Article]) { let objectIDs = articles.map { $0.objectID } let backgroundContext = container.newBackgroundContext()
backgroundContext.perform { for objectID in objectIDs { guard let article = try? backgroundContext.existingObject(with: objectID) else { continue } backgroundContext.delete(article) }
try? backgroundContext.save() }}Context Hierarchy and Parent Contexts
Section titled “Context Hierarchy and Parent Contexts”Child Context Pattern
Section titled “Child Context Pattern”// Parent context (view context)let parentContext = container.viewContext
// Child context for editinglet childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)childContext.parent = parentContext
// Make changes in childlet article = childContext.object(with: articleID) as! Articlearticle.name = "Updated"
// Save to parent (not to disk yet)try? childContext.save()
// Save parent to persisttry? parentContext.save()Benefits:
- Can discard changes by not saving child
- Useful for forms/editing
- Isolates changes
Caution:
- Adds complexity
- Two saves required for persistence
- Parent must be saved for changes to persist
Debugging Threading Issues
Section titled “Debugging Threading Issues”Enable Concurrency Debug
Section titled “Enable Concurrency Debug”Add launch argument:
-com.apple.CoreData.ConcurrencyDebug 1What it catches:
- Objects accessed from wrong thread
- Contexts used from wrong queue
- Thread safety violations
Example error:
CoreData: error: Serious application error.An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.*** -[NSManagedObjectContext performSelector:withObject:] called from thread which is not the context's thread with userInfo (null)Common Threading Errors
Section titled “Common Threading Errors”Error 1: Accessing Object from Wrong Context
Section titled “Error 1: Accessing Object from Wrong Context”// ❌ Wronglet article = viewContext.object(...)backgroundContext.perform { print(article.name) // CRASH!}
// ✅ Correctlet objectID = article.objectIDbackgroundContext.perform { if let article = try? backgroundContext.existingObject(with: objectID) as? Article { print(article.name) }}Error 2: Not Using perform
Section titled “Error 2: Not Using perform”// ❌ Wronglet backgroundContext = container.newBackgroundContext()let article = Article(context: backgroundContext) // CRASH!
// ✅ Correctlet backgroundContext = container.newBackgroundContext()backgroundContext.perform { let article = Article(context: backgroundContext)}Error 3: Passing Context Between Threads
Section titled “Error 3: Passing Context Between Threads”// ❌ WrongDispatchQueue.global().async { try? viewContext.save() // CRASH!}
// ✅ CorrectviewContext.perform { try? viewContext.save()}Merging Changes Between Contexts
Section titled “Merging Changes Between Contexts”Automatic Merging
Section titled “Automatic Merging”Enable automatic merging from parent:
context.automaticallyMergesChangesFromParent = trueBenefits:
- Changes from other contexts automatically merge
- No manual merge code needed
- Recommended for most cases
Manual Merging
Section titled “Manual Merging”Listen for save notifications:
NotificationCenter.default.addObserver( self, selector: #selector(contextDidSave), name: .NSManagedObjectContextDidSave, object: backgroundContext)
@objc func contextDidSave(_ notification: Notification) { viewContext.perform { viewContext.mergeChanges(fromContextDidSave: notification) }}Async/Await with Core Data (iOS 15+)
Section titled “Async/Await with Core Data (iOS 15+)”Using async/await
Section titled “Using async/await”func fetchArticles() async throws -> [Article] { let context = container.newBackgroundContext()
return try await context.perform { let fetchRequest = Article.fetchRequest() return try context.fetch(fetchRequest) }}
// UsageTask { do { let articles = try await fetchArticles() // Update UI with articles } catch { print("Failed to fetch: \(error)") }}Saving with async/await
Section titled “Saving with async/await”func saveArticle(name: String) async throws { let context = container.newBackgroundContext()
try await context.perform { let article = Article(context: context) article.name = name try context.save() }}Performance Considerations
Section titled “Performance Considerations”Context Reuse
Section titled “Context Reuse”// ❌ Bad: Creating new context for each operationfunc updateArticle1() { let context = container.newBackgroundContext() context.perform { /* ... */ }}
func updateArticle2() { let context = container.newBackgroundContext() // New context! context.perform { /* ... */ }}
// ✅ Better: Reuse context for related operationsclass DataManager { private lazy var backgroundContext = container.newBackgroundContext()
func updateArticle1() { backgroundContext.perform { /* ... */ } }
func updateArticle2() { backgroundContext.perform { /* ... */ } }}Context Reset
Section titled “Context Reset”For long-running contexts, periodically reset to free memory:
backgroundContext.perform { for (index, data) in largeDataset.enumerated() { let article = Article(context: backgroundContext) article.name = data.name
if index % 100 == 0 { try? backgroundContext.save() backgroundContext.reset() // Clear memory } }}Thread Confinement
Section titled “Thread Confinement”Each context is confined to its queue. You can call perform from any thread, but all Core Data work must run inside perform/performAndWait on that context.
let context = container.newBackgroundContext()
// ✅ Allowed: scheduling work from anywhereDispatchQueue.global().async { context.perform { // Work executes on context's queue }}
DispatchQueue.main.async { context.perform { // Also executes on context's queue }}
// ❌ Wrong: touching the context or its objects outside performDispatchQueue.global().async { let article = Article(context: context) // Not inside perform try? context.save() // Not inside perform}Common Pitfalls
Section titled “Common Pitfalls”❌ Passing Objects Directly
Section titled “❌ Passing Objects Directly”func updateInBackground(_ article: Article) { backgroundContext.perform { article.name = "Updated" // CRASH! }}❌ Not Using perform
Section titled “❌ Not Using perform”let backgroundContext = container.newBackgroundContext()let article = Article(context: backgroundContext) // CRASH!❌ Accessing UI from Background Context
Section titled “❌ Accessing UI from Background Context”backgroundContext.perform { let articles = try? backgroundContext.fetch(Article.fetchRequest()) tableView.reloadData() // CRASH! Wrong thread}❌ Using performAndWait on Main Thread
Section titled “❌ Using performAndWait on Main Thread”// On main threadbackgroundContext.performAndWait { // Heavy work - blocks UI!}✅ Correct Patterns
Section titled “✅ Correct Patterns”// Pass object IDsfunc updateInBackground(_ article: Article) { let objectID = article.objectID backgroundContext.perform { guard let article = try? backgroundContext.existingObject(with: objectID) as? Article else { return } article.name = "Updated" try? backgroundContext.save() }}
// Always use performlet backgroundContext = container.newBackgroundContext()backgroundContext.perform { let article = Article(context: backgroundContext)}
// Update UI on main threadbackgroundContext.perform { let articles = try? backgroundContext.fetch(Article.fetchRequest()) let objectIDs = articles?.map { $0.objectID } ?? []
DispatchQueue.main.async { // Update UI with objectIDs }}
// Use perform (async) instead of performAndWaitbackgroundContext.perform { // Heavy work doesn't block UI}Testing Threading
Section titled “Testing Threading”func testThreadSafety() { let expectation = XCTestExpectation(description: "Background save")
let objectID = article.objectID let backgroundContext = container.newBackgroundContext()
backgroundContext.perform { guard let article = try? backgroundContext.existingObject(with: objectID) as? Article else { XCTFail("Failed to fetch article") return }
article.name = "Updated"
do { try backgroundContext.save() expectation.fulfill() } catch { XCTFail("Failed to save: \(error)") } }
wait(for: [expectation], timeout: 5.0)}Summary
Section titled “Summary”- Never pass NSManagedObject between contexts - Always use NSManagedObjectID
- Always use
performorperformAndWait- Never access context directly - Prefer
performoverperformAndWait- Avoid blocking - Use view context for UI only - Heavy work in background contexts
- Enable
-com.apple.CoreData.ConcurrencyDebug 1- Catch threading violations - Enable
automaticallyMergesChangesFromParent- Automatic change propagation - Use async/await on iOS 15+ - Cleaner asynchronous code
- Reset contexts periodically - Free memory in long-running operations
- One context per queue - Don’t share contexts across queues
- Test threading behavior - Verify thread safety in tests