adventures in multithreaded core data
TRANSCRIPT
Adventures in Multithreaded Core Data
CocoaheadsBE - Zoersel, 2012-09-24
Introduction
Tom Adriaenssen
I love...
‣ ... my wife‣ ... my 4 kids‣ ... to code‣ ... to play a game of squash‣ ... good beer
I open sourced...... some code:
‣ IIViewDeckController: “An implementation of the sliding functionality found in the Path 2.0 or Facebook iOS apps.”
‣ IIDateExtensions
‣ IIPopoverStatusItem
See: http://github.com/inferis
I made...... some apps:
Butane DrashHi, @10to1!
http://getbutane.com http://dra.sh
Butane
‣ Official Campfire client kinda sucks, so we rolled our own
‣ Somewhat concurrent app
‣ Uses Core Data
‣ Together with 10to1
Campfire client for iOS
This presentation is about what I learned
while coding Butane.
Agenda
Agenda
‣ Core Data Primer‣ MagicalRecord‣ Concurrency problems‣ Concurrency solutions
Core Data Primer
What is: Core Data?
‣ Per the documentation:‣ The Core Data framework provides
generalized and automated solutions to common tasks associated with object life-‐cycle and object graph management, including persistence.
http://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/Articles/cdTechnologyOverview.html#//apple_ref/doc/uid/TP40009296-SW1
Wait, what?
What isn’t: Core Data?
‣ It’s not an RDBMS. ‣ If you want a database and SQL, use
Sqlite:
What isn’t: Core Data?
‣ It’s not just an ORM (Object Relational Mapper)
‣ It may look like there’s SQL under the hood, but that’s not necessarily the case.
So, what is it then?Core Data provides:‣ persistence‣ change tracking‣ relations (object graph)‣ lazy loading (“faulting”)‣ validation‣ works well with Cocoa (KVO, KVC)
Basically:
‣ A system to store data‣ Persistence agnostic (local storage,
iCloud, AFIncrementalStore, ...)‣ No need to write SQL to query‣ You can keep to Objective-C
Your tools:‣ NSPersistentStore
‣ NSManagedObjectContext
‣ NSManagedObject
‣ NSManagedObjectID
‣ NSFetchRequest
‣ NSEntityDescription
‣ NSPredicate
‣ NSSortDescription
A picture says a 1000 words...
MagicalRecord
MagicalRecord‣ Writing Core Data code is tedious. ‣ You need quite a bit of boilerplate code
to do something simple:
NSManagedObjectContext *moc = [self managedObjectContext];NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Employee" inManagedObjectContext:moc];NSFetchRequest *request = [NSFetchRequest new];[request setEntity:entityDescription]; NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"firstName" ascending:YES];[request setSortDescriptors:@[sortDescriptor]]; NSError *error;NSArray *array = [moc executeFetchRequest:request error:&error];
if (array){ // display items (eg in table view)}else { // Deal with error...}
MagicalRecord‣ MagicalRecord tries to solve this.
‣ = ActiveRecord pattern for Core Data.‣ Encapsulates the tediousness of plain
Core Data code.
MagicalRecord‣ Writing MagicalRecord enable code is
tedious no more:‣ That same example is now this:
NSManagedObjectContext *moc = [self managedObjectContext];NSArray *array = [Employee MR_findAllSortedBy:@"firstname" ascending:YES inContext:context];
if (array){ // display items (eg in table view)}else { // Deal with error...}
Great, isn’t it?
MagicalRecord+ write less code+ your code becomes more readable+ good for apps requiring simple storage scenarios (=most
apps, probably)+ hides complexity- hides complexity- easy to start, but diving deeper becomes harder- uses defaults that are suboptimal in a multithreaded app- concurrency errors and issues are subtle
MagicalRecord
‣ used MagicalRecord from the start‣ Took me a while to find out the problems
I was having were related to the complexity hiding
‣ The defaults are okay only for so much app complexity
MagicalRecord
‣ That said: Magical Record is great.‣ It will speed up your Core Data
development by several factors.‣ Also: take a look at mogenerator: ‣ http://rentzsch.github.com/mogenerator/
‣ or: brew install mogenerator
MagicalRecord
‣ So I threw it out.
MagicalRecord
‣ So I threw it out.‣ But not completely.
MagicalRecord
‣ So I threw it out.‣ But not completely.‣ More about that later.
The problems described hereafter only apply to the persistent stores with external backing
(for example: sqlite).
Concurrency: problems
Problems?
Problems?
‣ Core Data Objects are not thread safe.
Problems?
‣ Core Data Objects are not thread safe.‣ In essence: you can’t share them across
threads (except for NSManagedObjectID).
Problems?
‣ Core Data Objects are not thread safe.‣ In essence: you can’t share them across
threads (except for NSManagedObjectID).
‣ Core Data locks objects, even for read operations.
Object storage is locked for read operations, too‣ Objects used to power the UI must be
fetched on the UI thread.‣ Heavy/complex fetch requests (queries)
block the UI thread while fetching the objects. You don’t want that.
Objects aren’t supposed to be shared between threads‣ The NSManagedObjectContext “locks” an
object when you read one of its properties.‣ This can cause a deadlock when you do
access the same data from 2 threads.‣ Reason: faulting support can change the
object even while just reading from it.‣ You can’t turn it off.
Luckily, we can fix or workaround these problems.
Concurrency: solutions
Keep to your thread
Keep to your thread
‣ pre-iOS5: use thread confinement
Keep to your thread
‣ pre-iOS5: use thread confinement‣ iOS5 and later: use nested contexts
Thread confinement‣ In essence: keep an NSManagedObjectContext per
thread‣ Be very careful when going from one thread to
another.
‣ MagicalRecord tries to hide this from you:‣ It automatically provides a context for each thread‣ This is a bit counterintuitive since you start mixing
objects across threads quite easily.
Thread confinement
Image source: Cocoanetics
Nested contexts‣ Introduced in iOS5‣ Uses Grand Central Dispatch and dispatch queues‣ Core Data manages threading for you‣ Better than thread confinement‣ more straightforward‣ more flexible
‣ MagicalRecord hides this from you, too. ‣ Automatically switches to dispatch queues on iOS5 even
though the API remains the same.
Nested contexts
Image source: Cocoanetics
Nested contexts ‣ NSManagedObjectContext = cheap‣ You can nest contexts‣ Each context has its private dispatch
queue‣ No manual thread synchronization
necessary
Queue types‣ NSConfinementConcurrencyType‣ The old way (thread confinement)
‣ NSPrivateQueueConcurrencyType ‣ The context has its own private dispatch queue
‣ NSMainQueueConcurrencyType ‣ The context is associated with the main queue (or runloop, or UI
thread)
parentMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];[parentMoc setPersistentStoreCoordinator:coordinator];
moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];moc.parentContext = parentMoc;
Thread safe?‣ performBlock: performBlockAndWait:
‣ Run a block you provide on the queue associated with the context.‣ Object access in the block is thread safe
[context performBlockAndWait:^{ for (Room* room in [Room findAllInContext:context]) { room.joinedUsers = [NSSet set]; room.knowsAboutJoinedUsersValue = NO; room.unreadCountValue = 0; room.status = @""; } NSError* error = nil; [context save:&error];}];
performBlock, and wait-‐ (void)performBlock:(void (^)())block;
‣ executes the block on the context dispatch queue as soon as possible
‣ nonblocking call
‣ code will not execute immediately
-‐ (void)performBlockAndWait:(void (^)())block;
‣ executes the block on the context dispatch queue immediately
‣ blocks the current execution until block is done
‣ can cause a deadlock (if you’re already running code on the same queue)
When to use what?‣ performBlock:
‣ For actions which are “on their own”
‣ Consider the code in the block a Unit Of Work‣ Best for save operations‣ Useful for long fetches (use callbacks)
‣ performBlockAndWait:
‣ When you need stuff immediately‣ Good for small fetches or “standalone” saves
How is this better than thread confinement?
‣ No manual thread handling, Core Data handles it for you.
‣ More flexible: as long as you access managed objects in the correct context using performBlock: you’re pretty safe‣ also applies to the main/UI thread! (unless
you’re sure you’re on the main thread)
Saving nested contexts
‣ Saves are only persisted one level deep.‣ Parent contexts don’t pull changes from
child contexts.‣ Child contexts don’t see changes by
parent contexts.‣ Make them plenty and short-lived
How to nest contexts
‣ 2 approaches:‣ root context = NSMainQueueConcurrencyType
‣ root context = NSPrivateQueueConcurrencyType
Root = Main
Image source: Cocoanetics
‣ Many child contents with private queues
‣ Root context on main queue
‣ Actual persistence happens on main queue, could block the UI
Root = Private
Image source: Cocoanetics
‣ Root context with private queue
‣ Many child contents with private queues
‣ context on main queue is child of root
‣ Actual persistence happens in background (does not block the UI)
What problems did we have again?
What problems did we have again?
‣ No sharing of NSManagedObjects between threads.
What problems did we have again?
‣ No sharing of NSManagedObjects between threads.
‣ Context locking
Sharing: solution‣ Pass only NSManagedObjectIDs to
other threads, not objects.‣ Refetch the object on a different thread
or queue to work with it.‣ Don’t forget to save the ‘original’ object
first before working with it on the second thread or queue.
Complex queries‣ Use the same technique for complex or large queries.
1. do the heavy lifting in the background2. pass list of NSManagedObjectIDs to another thread
(e.g. UI thread).3. load objects as faults, and let Core Data fault them in
when you need them (e.g. when accessing a property)
‣ That’s lot of requests, but this is actually more performant and desirable in most cases.
Locking: solution‣ Use child contexts with their own
dispatch queues.‣ Use: performBlock: and performBlockAndWait: carefully.
‣ Deadlocks still possible, especially with performBlockAndWait:
A word about NSManagedObjectIDs‣ Two types of IDs:‣ temporary ‣ when the object hasn’t been persisted to a
store
‣ permanent‣ when the object has been persisted to a
store
A word about NSManagedObjectIDs‣ Subtle bug: temporary IDs from a non-root
context don’t get updated to permanent IDs when saving in the root context
‣ The object is saved just fine, but the ID is not updated correctly.
‣ When passing these around to other contexts after saving: you won’t find the object in another child context!
A word about NSManagedObjectIDs
‣ To the rescue: -‐ (BOOL)obtainPermanentIDsForObjects:
(NSArray *)objects error:(NSError **)error;
A word about NSManagedObjectIDs
-‐ (BOOL)obtainPermanentIDsForObjects:(NSArray *)objects error:(NSError **)error;
‣ Transforms objects with temporary IDs to permanent IDs (through the persistent store of the root context).
‣ Do this when creating a managed object and you’re safe.
‣ Obtaining permanentIDs is batched, so the performance hit is not that high
MagicalRecord‣ I still use MagicalRecord:‣ Reduced form: no more “automatic”
context handling --> dangerous! ‣ Added some extra sauce to work with
the nested contexts.‣ The methods MR supplies still allow for
a speedup when coding.
Useful References
‣ Nested context release notes: http://developer.apple.com/library/mac/#releasenotes/DataManagement/RN-CoreData/_index.html
‣ Magical Record: https://github.com/magicalpanda/MagicalRecord‣ Mogenerator: http://rentzsch.github.com/mogenerator/‣ A good read on Cocoanetics: http://www.cocoanetics.com/2012/07/multi-context-coredata/
‣ Core data programming guide: http://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/cdProgrammingGuide.html
‣ Butane: http://getbutane.com
Thanks for listening.
Twitter: @inferisApp.Net: @inferisE-mail: [email protected]: http://inferis.org
Questions? Contact me: