I'm little bit confused about Core Data multithreading saving.
I have following NSManagedObjectContext
setup (same as MagicalRecord
):
SavingContext (NSPrivateQueueConcurrencyType) has child DefaultContext(NSMainQueueConcurrencyType)
Each saving thread has own context (NSPrivateQueueConcurrencyType
) with DefaultContext
as parent.
So the question is: how can I rely on saving same type on different threads if I need to guarantee uniqueness?
Here is small test example (Test is subclass of NSManagedObject):
@implementation Test
+ (instancetype) testWithValue:(NSString *) str {
[NSThread sleepForTimeInterval:3];
Test *t = [Test MR_findFirstByAttribute:@"uniqueField" withValue:str];
if (!t) {
NSLog(@"No test found!");
t = [Test MR_createEntity];
}
t.uniqueField = str;
return t;
}
@end
It first checks if there is a Test
in newly created thread context (which has parent DefaultContext
) and if no - create it in current thread context.
And here is the test code:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;
[queue addOperationWithBlock:^{
[Test operationWithValue:@"1"];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}];
[queue addOperationWithBlock:^{
[Test operationWithValue:@"1"];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Total tests: %lu", (unsigned long)[Test MR_countOfEntities]);
[Test MR_truncateAll];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
});
It just run two operations, and trying to save same data. After creating Test I save all contexts (current thread, default context and root saving context). Most of the time there will be 2 tests. You can modify and add semaphore to ensure both threads reach checking at the same time.
Update 20.09.2014
I've added the code, provided by mitrenegade (see below) and now my Test.m
has a function:
-(BOOL)validateUniqueField:(id *)ioValue error:(NSError **)outError {
// The property being validated must not already exist
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([self class])];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@ AND self != %@", *ioValue, self];
int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
if (count > 0) {
NSLog(@"Thread: %@ (isMain: %hhd), Validation failed!", [NSThread currentThread], [NSThread isMainThread]);
return NO;
}
NSLog(@"Thread: %@ (isMain: %hhd), Validation succeeded!", [NSThread currentThread], [NSThread isMainThread]);
return YES;
}
With two threads creating the same value (test sample is in the beginning of post) I have the following output:
2014-09-20 11:48:53.824 coreDataTest[892:289814] Thread: <NSThread: 0x15d38940>{number = 3, name = (null)} (isMain: 0), Validation succeeded!
2014-09-20 11:48:53.826 coreDataTest[892:289815] Thread: <NSThread: 0x15e434a0>{number = 2, name = (null)} (isMain: 0), Validation succeeded!
2014-09-20 11:48:53.830 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.833 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.837 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.839 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:56.251 coreDataTest[892:289750] Total tests: 2
But if I look at underlying sqlite file there is no records at all (that means they stuck in Main Context)