adventures in multithreaded core data

73
Adventures in Multithreaded Core Data CocoaheadsBE - Zoersel, 2012-09-24

Upload: inferis

Post on 12-May-2015

28.588 views

Category:

Technology


1 download

TRANSCRIPT

Page 1: Adventures in Multithreaded Core Data

Adventures in Multithreaded Core Data

CocoaheadsBE - Zoersel, 2012-09-24

Page 2: Adventures in Multithreaded Core Data

Introduction

Page 3: Adventures in Multithreaded Core Data
Page 4: Adventures in Multithreaded Core Data

Tom Adriaenssen

Page 5: Adventures in Multithreaded Core Data

I love...

‣ ... my wife‣ ... my 4 kids‣ ... to code‣ ... to play a game of squash‣ ... good beer

Page 6: Adventures in Multithreaded Core Data

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

Page 7: Adventures in Multithreaded Core Data

I made...... some apps:

Butane DrashHi, @10to1!

http://getbutane.com http://dra.sh

Page 8: Adventures in Multithreaded Core Data

Butane

‣ Official Campfire client kinda sucks, so we rolled our own

‣ Somewhat concurrent app

‣ Uses Core Data

‣ Together with 10to1

Campfire client for iOS

Page 9: Adventures in Multithreaded Core Data

This presentation is about what I learned

while coding Butane.

Page 10: Adventures in Multithreaded Core Data

Agenda

Page 11: Adventures in Multithreaded Core Data

Agenda

‣ Core Data Primer‣ MagicalRecord‣ Concurrency problems‣ Concurrency solutions

Page 12: Adventures in Multithreaded Core Data

Core Data Primer

Page 13: Adventures in Multithreaded Core Data

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

Page 14: Adventures in Multithreaded Core Data

Wait, what?

Page 15: Adventures in Multithreaded Core Data

What isn’t: Core Data?

‣ It’s not an RDBMS. ‣ If you want a database and SQL, use

Sqlite:

Page 16: Adventures in Multithreaded Core Data

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.

Page 17: Adventures in Multithreaded Core Data

So, what is it then?Core Data provides:‣ persistence‣ change tracking‣ relations (object graph)‣ lazy loading (“faulting”)‣ validation‣ works well with Cocoa (KVO, KVC)

Page 18: Adventures in Multithreaded Core Data

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

Page 19: Adventures in Multithreaded Core Data

Your tools:‣ NSPersistentStore

‣ NSManagedObjectContext

‣ NSManagedObject

‣ NSManagedObjectID  

‣ NSFetchRequest

‣ NSEntityDescription

‣ NSPredicate

‣ NSSortDescription

Page 20: Adventures in Multithreaded Core Data

A picture says a 1000 words...

Page 21: Adventures in Multithreaded Core Data

MagicalRecord

Page 22: Adventures in Multithreaded Core Data

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...}

Page 23: Adventures in Multithreaded Core Data

MagicalRecord‣ MagicalRecord tries to solve this.

‣ = ActiveRecord pattern for Core Data.‣ Encapsulates the tediousness of plain

Core Data code.

Page 24: Adventures in Multithreaded Core Data

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...}

Page 25: Adventures in Multithreaded Core Data
Page 26: Adventures in Multithreaded Core Data

Great, isn’t it?

Page 27: Adventures in Multithreaded Core Data

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

Page 28: Adventures in Multithreaded Core Data

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

Page 29: Adventures in Multithreaded Core Data

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

Page 30: Adventures in Multithreaded Core Data

MagicalRecord

‣ So I threw it out.

Page 31: Adventures in Multithreaded Core Data

MagicalRecord

‣ So I threw it out.‣ But not completely.

Page 32: Adventures in Multithreaded Core Data

MagicalRecord

‣ So I threw it out.‣ But not completely.‣ More about that later.

Page 33: Adventures in Multithreaded Core Data

The problems described hereafter only apply to the persistent stores with external backing

(for example: sqlite).

Page 34: Adventures in Multithreaded Core Data

Concurrency: problems

Page 35: Adventures in Multithreaded Core Data

Problems?

Page 36: Adventures in Multithreaded Core Data

Problems?

‣ Core Data Objects are not thread safe.

Page 37: Adventures in Multithreaded Core Data

Problems?

‣ Core Data Objects are not thread safe.‣ In essence: you can’t share them across

threads (except for NSManagedObjectID).

Page 38: Adventures in Multithreaded Core Data

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.

Page 39: Adventures in Multithreaded Core Data

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.

Page 40: Adventures in Multithreaded Core Data

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.

Page 41: Adventures in Multithreaded Core Data
Page 42: Adventures in Multithreaded Core Data

Luckily, we can fix or workaround these problems.

Page 43: Adventures in Multithreaded Core Data

Concurrency: solutions

Page 44: Adventures in Multithreaded Core Data

Keep to your thread

Page 45: Adventures in Multithreaded Core Data

Keep to your thread

‣ pre-iOS5: use thread confinement

Page 46: Adventures in Multithreaded Core Data

Keep to your thread

‣ pre-iOS5: use thread confinement‣ iOS5 and later: use nested contexts

Page 47: Adventures in Multithreaded Core Data

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.

Page 48: Adventures in Multithreaded Core Data

Thread confinement

Image source: Cocoanetics

Page 49: Adventures in Multithreaded Core Data

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.

Page 50: Adventures in Multithreaded Core Data

Nested contexts

Image source: Cocoanetics

Page 51: Adventures in Multithreaded Core Data

Nested contexts ‣ NSManagedObjectContext = cheap‣ You can nest contexts‣ Each context has its private dispatch

queue‣ No manual thread synchronization

necessary

Page 52: Adventures in Multithreaded Core Data

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;

Page 53: Adventures in Multithreaded Core Data

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];}];

Page 54: Adventures in Multithreaded Core Data

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)

Page 55: Adventures in Multithreaded Core Data

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

Page 56: Adventures in Multithreaded Core Data

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)

Page 57: Adventures in Multithreaded Core Data

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

Page 58: Adventures in Multithreaded Core Data

How to nest contexts

‣ 2 approaches:‣ root context = NSMainQueueConcurrencyType

‣ root context = NSPrivateQueueConcurrencyType

Page 59: Adventures in Multithreaded Core Data

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

Page 60: Adventures in Multithreaded Core Data

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)

Page 61: Adventures in Multithreaded Core Data

What problems did we have again?

Page 62: Adventures in Multithreaded Core Data

What problems did we have again?

‣ No sharing of NSManagedObjects between threads.

Page 63: Adventures in Multithreaded Core Data

What problems did we have again?

‣ No sharing of NSManagedObjects between threads.

‣ Context locking

Page 64: Adventures in Multithreaded Core Data

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.

Page 65: Adventures in Multithreaded Core Data

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.

Page 66: Adventures in Multithreaded Core Data

Locking: solution‣ Use child contexts with their own

dispatch queues.‣ Use: performBlock: and performBlockAndWait:  carefully.

‣ Deadlocks still possible, especially with performBlockAndWait:

Page 67: Adventures in Multithreaded Core Data

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

Page 68: Adventures in Multithreaded Core Data

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!

Page 69: Adventures in Multithreaded Core Data

A word about NSManagedObjectIDs

‣ To the rescue: -­‐  (BOOL)obtainPermanentIDsForObjects:

(NSArray  *)objects  error:(NSError  **)error;

Page 70: Adventures in Multithreaded Core Data

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

Page 71: Adventures in Multithreaded Core Data

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.

Page 72: Adventures in Multithreaded Core Data

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

Page 73: Adventures in Multithreaded Core Data

Thanks for listening.

Twitter: @inferisApp.Net: @inferisE-mail: [email protected]: http://inferis.org

Questions? Contact me: