First, Core Data is thread safe. However you must follow the rules:
NSManagedObjectContext
is thread bound. You can only use it on the thread it is assigned to. -init
causes a context to be assigned to the thread it is created on. Using -initWithConcurrencyType:
will allow you to create contexts associated to other threads/queues.
- Any
NSManagedObject
associated with a NSManagedObjectContext
is tied to the same thread/queue as the context it came from
- There is no third rule
You can pass NSManagedObjectID
instances between threads but rules 1 and 2 must be obeyed. From your description I believe you are violating these rules.
Personally I do not recommend using NSManagedObjectID either. There are better solutions. – Marcus S. Zarra
Marcus, this is the most succinct explanation of Core Data's threading I've read. Having used it since it's introduction, there are days I still get these rules wrong! You mention "better solutions" — can you elaborate?
I have a fairly strong distrust of the use of the NSManagedObjectID
. It does not stay the same from one application lifecycle to another in many situations. Originally, based on the documentation, we (Cocoa developers in general) believed it was our mythical primary key being generated for us. That has turned out to be incorrect.
In modern development with parent/child contexts the landscape is even more confusing and there are some interesting traps that we need to watch out for. Given the current landscape I dislike it more than I did previously. So what do we use?
We should generate our own. It doesn't need to be much. If your data does not have a primary key from the server already (pretty common to have an id
from a Ruby based server) then create one. I like to call it guid
and then have an -awakeFromInsert
similar to this:
- (void)awakeFromInsert
{
[super awakeFromInsert];
if (![self primitiveValueForKey:@"guid"]) {
[self setPrimitiveValue:[[NSProcessInfo processInfo] globallyUniqueString] forKey:@"guid"];
}
}
NOTE: This code is written in the web browser and may not compile.
You check the value because -awakeFromInsert
is called once per context. Then I will generally have a convenience method on my NSManagedObject
instances similar to:
@implementation MyManagedObject
+ (MyManagedObject*)managedObjectForGUID:(NSString*)guid inManagedObjectContext:(NSManagedObjectContext*)context withError:(NSError**)error
{
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:[self entityName]];
[fetch setPredicate:[NSPredicate predicateWithFormat:@"guid == %@", guid]];
NSArray *results = [context executeFetchRequest:request error:error];
if (!results) return nil;
return [results lastObject];
}
@end
NOTE: This code is written in the web browser and may not compile.
This leaves error handling and context/threading control up to the developer but provides a convenience method to retrieve an object on the current context and lets us "bounce" an object from one context to another.
This is slower than -objectWithID:
and should be used carefully and only in situations where you need to bounce an object from one context to another after a save moves it up the stack.
As with most things I do; this is not a generic solution. It is a baseline that should get adjusted on a per project basis.