3

I am using the method described in Core Data unique attributes to prevent having the same attribute twice (the attribute is called ID here).

This works fine in a single-threaded environment. I use a multi-threaded environment and use the one context per thread paradigm.

The problem is that if two threads simultaneously try to create an object with the same attribute, there is the following problem:

  • Thread A creates the object with ID 1 in context1, there is no such object so it gets created
  • Thread B creates the object with ID 1 in context2, there is no such object so it gets created
  • Thread A synchronizes the context1->context2 (using the code below)

You find yourself with two records with the same ID (1).

I saw this problem when testing, so it is rare but will certainly happen over time.

Of course, there are multiple options, like GDC, semaphores to prevent this from happening but before going with a complex solution, I was wondering if someone has a better solution, or could recommend at what level to serialize the events.

Using iOS5+ and ARC.

I use this code for the synch of my contexts:

[[NSNotificationCenter defaultCenter] addObserver:self 
selector:@selector(backgroundContextDidSave:) 
name:NSManagedObjectContextDidSaveNotification                                                          
object:nil];

and:

- (void)backgroundContextDidSave:(NSNotification *)notification {
    /* Make sure we're on the main thread when updating the main context */
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(backgroundContextDidSave:)
                               withObject:notification
                            waitUntilDone:NO];
        return;
    }

    /* merge in the changes to the main context */
    for (NSManagedObjectContext* context in [self.threadsDictionary allValues]){
            [context mergeChangesFromContextDidSaveNotification:notification];
    }

}

To get a thread-safe context:

/**
 Returns the managed object context for the application.
 If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
 */
- (NSManagedObjectContext *) managedObjectContextForThread {

    // Per thread, give one back
    NSString* threadName = [NSString stringWithFormat:@"%d",[NSThread currentThread].hash];

    NSManagedObjectContext * existingContext = [self.threadsDictionary objectForKey:threadName];
    if (existingContext==nil){
        existingContext = [[NSManagedObjectContext alloc] init];
        [existingContext setPersistentStoreCoordinator: [self persistentStoreCoordinator]];
        [self.threadsDictionary setValue:existingContext forKey:threadName];
        [existingContext setMergePolicy:NSOverwriteMergePolicy];
    }

    return existingContext;
}
Community
  • 1
  • 1
Resh32
  • 6,500
  • 3
  • 32
  • 40
  • If your IDs don't have to be integers, you could simply create UUIDs for your object IDs. Check out `CFUUIDCreate` and friends... – nielsbot Sep 21 '12 at 09:01
  • @nielsbot, yes they are integers, used on the back-end of my system as well... – Resh32 Sep 21 '12 at 11:33

2 Answers2

0

I think that I found the solution, using Core Data only.

I am now using NSLock to lock the contexts to simulate some sort of a transaction:

[[self managedObjectContextForThread] tryLock];

... read if ID1 exists ...

... write ID1 ...

[[self managedObjectContextForThread] unlock];

This seems to solve the problem (for now).

Resh32
  • 6,500
  • 3
  • 32
  • 40
0

I have a related problem, but I'm using much newer Core Data approach - using context hierarchies, which should have resolved synchronization issues like this.

In my case, I have an internal crash within CoreData, when I'm trying to save, and CoreData context is validating the attribute for uniqueness. My validation is implemented in the KVC property validation method:

// Validate the catalogID for uniqueness. This implementation works fine in the following scenarios:
//  * end-editing,
//  * saving a new species entity,
//  * updating existing species entity.
-(BOOL)validateCatalogID:(id *)ioValue error:(NSError * __autoreleasing *)outError {

if (*ioValue == nil)
    return YES; // Let CoreData validate for empty/missing values

// Lazily prepare static request and predicate to fetch OTHER species catalogID's. We'll reuse these every time we validate the edited species catalogID.
static NSFetchRequest *otherSpeciesIDsFetchRequest = nil;
static NSPredicate *otherSpeciesCatalogIDsPredicate = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    otherSpeciesIDsFetchRequest = [NSFetchRequest fetchRequestWithEntityName:[[self entity] name]];
    otherSpeciesCatalogIDsPredicate = [NSPredicate predicateWithFormat:@"(catalogID == $ID) AND (self != $THIS_SPECIES)"];
});

@synchronized(otherSpeciesIDsFetchRequest) {
    // Configure the predicate with the current catalogID value to validate.
    otherSpeciesIDsFetchRequest.predicate = [otherSpeciesCatalogIDsPredicate predicateWithSubstitutionVariables:@{@"ID":*ioValue, @"THIS_SPECIES":self}];

    // instead of fectching entity data, we only COUNT matching items.
    NSError *error = nil;
    NSInteger count = [self.managedObjectContext countForFetchRequest:otherSpeciesIDsFetchRequest error:&error];
//    DBGLog(@"Check Species ID:%@ uniqueness, found %ld", *ioValue, (long)count);
    if (count > 0) {
        [self addValidationError:outError
                            code:ePMXexistingCatalogID
                         message:[NSString stringWithFormat:NSLocalizedString(@"ID '%@' is already used for another species. Use a unique ID", NULL), *ioValue]
                            ];
        return NO;
    }
    else {
        if (error!=nil) {
            [self addValidationError:outError code:ePMXexistingCatalogID
                             message:[NSString stringWithFormat:NSLocalizedString(@"Species ID '%@' cannot be validated. error %@", NULL), *ioValue, error]];
            return NO;
        }
    }
    return YES; // this catalogID was not found in any other species entity - unique - valid catalogID.
}
}

The crash I'm seeing looks like this in the stack:

Crashed Thread:  0  Dispatch queue: com.apple.main-thread

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: EXC_I386_GPFLT

Application Specific Information:
objc_msgSend() selector name: subentitiesByName


 Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
 0   libobjc.A.dylib               objc_msgSend + 23
 1   com.apple.CoreData            -[NSManagedObjectContext countForFetchRequest:error:] + 235
 2   com.IOLR.PlanktoMetrix-II     -[PMSpecies(PMExtensions) validateCatalogID:error:] + 559
 3   com.apple.CoreData            -[NSManagedObject(_NSInternalMethods) _validateValue:forProperty:andKey:withIndex:error:] + 243
 4   com.apple.CoreData            -[NSManagedObject(_NSInternalMethods) _validatePropertiesWithError:] + 314
 5   com.apple.CoreData            -[NSManagedObject(_NSInternalMethods) _validateForSave:] + 106
 6   com.apple.CoreData            -[NSManagedObject validateForUpdate:] + 64
 7   com.IOLR.PlanktoMetrix-II     -[PMSpecies(PMExtensions) validateForUpdate:] + 73
 8   com.apple.CoreData            -[NSManagedObjectContext(_NSInternalAdditions) _validateObjects:forOperation:error:exhaustive:forSave:] + 619
 9   com.apple.CoreData            -[NSManagedObjectContext(_NSInternalAdditions) _validateChangesForSave:] + 410
 10  com.apple.CoreData            -[NSManagedObjectContext(_NSInternalChangeProcessing) _prepareForPushChanges:] + 208
 11  com.apple.CoreData            -[NSManagedObjectContext save:] + 247
 12  com.IOLR.PlanktoMetrix-II     -[BSManagedDocument contentsForURL:ofType:saveOperation:error:] + 918
 13  com.IOLR.PlanktoMetrix-II     __73-[BSManagedDocument saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke + 411
 14  com.apple.AppKit              -[NSDocument performAsynchronousFileAccessUsingBlock:] + 57
 15  com.IOLR.PlanktoMetrix-II     -[BSManagedDocument saveToURL:ofType:forSaveOperation:completionHandler:] + 370
 16  com.apple.AppKit              __67-[NSDocument autosaveWithImplicitCancellability:completionHandler:]_block_invoke + 1220
 17  com.apple.AppKit              -[NSDocument continueFileAccessUsingBlock:] + 234
 18  com.apple.AppKit              __54-[NSDocument performAsynchronousFileAccessUsingBlock:]_block_invoke681 + 125
 19  com.apple.AppKit              __62-[NSDocumentController(NSInternal) _onMainThreadInvokeWorker:]_block_invoke1807 + 175

Having solved a similar problem can you hint about my implementation?

Motti Shneor
  • 2,095
  • 1
  • 18
  • 24