I'm getting locks in Core Data. I really don't understand the reason. Because I'm creating a background MOC when I'm processing in a background thread. Below you can see what a stack trace (I'm pausing the execution of the app) looks like when this happens:
Thread 1, Queue : com.apple.main-thread #0 0x32d2a0fc in __psynch_mutexwait () #1 0x3608b128 in pthread_mutex_lock () #2 0x365d2dac in -[_PFLock lock] () #3 0x365e3264 in -[NSPersistentStoreCoordinator executeRequest:withContext:error:] () #4 0x365e1e2a in -[NSManagedObjectContext executeFetchRequest:error:] () #5 0x3664a93e in -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:] () #6 0x3664b0c8 in __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke_0 () #7 0x3932bd28 in _dispatch_barrier_sync_f_slow_invoke () Thread 10, Queue : EventKitHelperSyncSerialBackgroundQueue #0 0x32d19f04 in semaphore_wait_trap () #1 0x3932c300 in _dispatch_thread_semaphore_wait$VARIANT$mp () #2 0x3932a880 in _dispatch_barrier_sync_f_slow () #3 0x3663b9e6 in _perform () #4 0x3664adba in -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] () #5 0x365e1e2a in -[NSManagedObjectContext executeFetchRequest:error:] () #6 0x000b11e4 in -[CoreDataHelper fetchEntity:predicate:andSortDescriptors:inManagedObjectContext:] at /Users/peterwarbo/Desktop/app/CoreDataHelper.m:110 #7 0x000ad648 in -[EventKitHelper processChangedCalendar] at /Users/peterwarbo/Desktop/app/EventKitHelper.m:242 #8 0x000ad3b4 in __54-[EventKitHelper syncInBackgroundWithCompletionBlock:]_block_invoke_0 at /Users/peterwarbo/Desktop/app/EventKitHelper.m:218 #9 0x3932711e in _dispatch_call_block_and_release () #10 0x3932aece in _dispatch_queue_drain$VARIANT$mp () #11 0x3932adc0 in _dispatch_queue_invoke$VARIANT$mp () #12 0x3932b91c in _dispatch_root_queue_drain () #13 0x3932bac0 in _dispatch_worker_thread2 () #14 0x36090a10 in _pthread_wqthread () #15 0x360908a4 in start_wqthread ()
In the EventKitHelperSyncSerialBackgroundQueue
I'm doing some Core Data processing in a background queue. Reminder
s are NSManagedObject
s. Sorry for the amount of code but I thought it's better to not leave out any important details.
EventKitHelper.m
- (void)syncInBackgroundWithCompletionBlock:(CalendarSyncCompletionBlock)block { DLogName() self.completionBlock = block; if (self.syncSerialBackgroundQueue == NULL) { self.syncSerialBackgroundQueue = dispatch_queue_create("EventKitHelperSyncSerialBackgroundQueue", 0); } dispatch_async(self.syncSerialBackgroundQueue, ^{ [self processChangedCalendar]; }); } - (void)processChangedCalendar { DLogName() CoreDataHelper *cdHelper = [CoreDataHelper sharedInstance]; // Store has been changed, events could be updated/deleted/added // Need to check if any of the user created Reminders are referencing the calendar // If so, update the affected Reminders // Predicate to fetch only Reminders that are of type (RMReminderDateServiceCalendarEvent or RMReminderDateServiceCalendarBirthday) AND status is not completed NSPredicate *userRemindersPredicate = [NSPredicate predicateWithFormat:@"(dateService == %@ OR dateService == %@) AND status != %@", @(RMReminderDateServiceCalendarEvent), @(RMReminderDateServiceCalendarBirthday), @(RMReminderStatusCompleted)]; // Sort the user's Reminders with the earliest date first NSSortDescriptor *dateSortAsc = [NSSortDescriptor sortDescriptorWithKey:@"date" ascending:YES]; // Creating a new MOC for thread safety NSManagedObjectContext *syncContext = [cdHelper threadedManagedObjectContext]; self.syncContext = syncContext; NSArray *usersReminders = [[CoreDataHelper sharedInstance] fetchEntity:APReminderEntity predicate:userRemindersPredicate andSortDescriptors:@[dateSortAsc] inManagedObjectContext:syncContext]; if (usersReminders.count == 0) { DLog(@"User doesn't have any Calendar Reminders, no need to sync") BOOL error = NO; self.completionBlock(error); return; } else { if (!self.isCalendarAccessAuthorized) { DLog(@"Calendar access is not authorized and we have Calendar Reminders, alert the user") BOOL error = YES; self.completionBlock(error); return; } else { DLog(@"Calendar access is authorized") } } if (!self.calendarchanged) { DLog(@"Calendar not updated, no need to sync") BOOL error = NO; self.completionBlock(error); return; } DLog(@"Calendar updated, syncing...") NSDate *earliestReminderDate = [(Reminder *) [usersReminders objectAtIndex:0] date]; // Since there exists a possibility that a Calendar event can change date back in time, we should fetch events from our earliest Reminder date + 1 year back NSDate *eventsFromThisDate = [Utilities oneYearAgoForDate:[Utilities midnightDateForDate:earliestReminderDate]]; NSDate *endDate = [NSDate distantFuture]; // This will get me events 4 years from now // Create the predicate NSPredicate *eventStorePredicate = [self.eventStore predicateForEventsWithStartDate:eventsFromThisDate endDate:endDate calendars:nil]; // Fetch all events that match the predicate. NSArray *eventKitEvents = [self.eventStore eventsMatchingPredicate:eventStorePredicate]; NSMutableArray *events = [NSMutableArray arrayWithCapacity:100]; for (EKEvent *event in eventKitEvents) { NSString *eventTitle = [event title]; NSDate *eventDate = [event startDate]; NSDate *eventDateModified = [event lastModifiedDate]; NSString *eventID = [event eventIdentifier]; // Check if event is a Birthday event BOOL isBirthday = [event birthdayPersonID] != -1 ? YES : NO; RMReminderDateService dateService; if (isBirthday) { dateService = RMReminderDateServiceCalendarBirthday; } else { dateService = RMReminderDateServiceCalendarEvent; } RMDateEvent *calendarEvent = [[RMDateEvent alloc] initWithDate:eventDate dateModified:eventDateModified name:eventTitle dateService:dateService andID:eventID]; BOOL eventAlreadyAdded = NO; if (!eventAlreadyAdded) { [events addObject:calendarEvent]; } } for (Reminder *reminder in usersReminders) { NSPredicate *predicateID = [NSPredicate predicateWithFormat:@"ID == %@", reminder.dateServiceID]; NSArray *eventsMatchingID = [events filteredArrayUsingPredicate:predicateID]; RMDateEvent *event = [eventsMatchingID lastObject]; if (event == nil) { // We couldn't find the event by ID, try to find it by date AND title NSPredicate *predicateDateAndTitle = [NSPredicate predicateWithFormat:@"date == %@ AND name == %@", reminder.date, reminder.dateText]; NSArray *eventsMatchingDateAndTitle = [events filteredArrayUsingPredicate:predicateDateAndTitle]; event = [eventsMatchingDateAndTitle lastObject]; if (event == nil) { // We couldn't find the event, most likely it has been deleted from the user's events or the user has changed all values for our saved event :-( } else { // We found it by date AND title [self processReminder:reminder forDateEvent:event]; } } else { // We found it by ID [self processReminder:reminder forDateEvent:event]; } } [self fetchEventsFromNow]; [self processEventKitEvents]; #warning TODO: Broadcast a message to update the Reminder date AppDelegate *appDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate]; [appDelegate setTabCountInBackground]; self.calendarchanged = NO; DLog(@"Calendar sync done") BOOL error = NO; self.completionBlock(error); } - (void)processReminder:(Reminder *)reminder forDateEvent:(RMDateEvent *)event { NSDate *eventModifiedDate = [event dateModified]; if ([eventModifiedDate compare:reminder.dateModified] == NSOrderedDescending) { // This event has been modified // Most important now is to check if the changed event date has passed NSDate *today = [NSDate date]; if ([today compare:event.date] == NSOrderedDescending) { // Event date has passed if (reminder.isRepeating) { // We cancel the UILocalNotification and reschedule a new UILocalNotification for the next Reminder date status also set to overdue NSDate *reminderDate = [Utilities reminderDateFromDate:event.date andTime:reminder.date]; // Cancel UILocalNotification [Utilities cancelUILocalNotificationForReminder:reminder]; reminder.status = @(RMReminderStatusOverdue); reminder.date = reminderDate; reminder.dateModified = event.dateModified; reminder.dateServiceID = event.ID; reminder.dateText = event.name; NSDate *nextReminderDate = [Utilities nextReminderDateFromNowForReminder:reminder]; reminder.date = nextReminderDate; // Re-schedule the Reminder [Utilities scheduleUILocalNotificationForReminder:reminder]; // We change back to this old Reminder date to reflect the overdue status reminder.date = reminderDate; [[CoreDataHelper sharedInstance] saveInManagedObjectContext:self.syncContext]; } else { // We should cancel the UILocalNotification for this Reminder and set the status for this Reminder to overdue NSDate *reminderDate = [Utilities reminderDateFromDate:event.date andTime:reminder.date]; // Cancel UILocalNotification [Utilities cancelUILocalNotificationForReminder:reminder]; reminder.status = @(RMReminderStatusOverdue); reminder.date = reminderDate; reminder.dateModified = event.dateModified; reminder.dateServiceID = event.ID; reminder.dateText = event.name; [[CoreDataHelper sharedInstance] saveInManagedObjectContext:self.syncContext]; } } else { // Event date is in the future NSDate *reminderDate = [Utilities reminderDateFromDate:event.date andTime:reminder.date]; // Cancel UILocalNotification [Utilities cancelUILocalNotificationForReminder:reminder]; reminder.status = @(RMReminderStatusUpcoming); reminder.date = reminderDate; reminder.dateModified = event.dateModified; reminder.dateServiceID = event.ID; reminder.dateText = event.name; [[CoreDataHelper sharedInstance] saveInManagedObjectContext:self.syncContext]; // Re-schedule the Reminder [Utilities scheduleUILocalNotificationForReminder:reminder]; } } }
CoreDataHelper.m
- (NSArray *)fetchEntity:(NSString *)entity predicate:(NSPredicate *)predicate andSortDescriptors:(NSArray *)sortDescriptors inManagedObjectContext:(NSManagedObjectContext *)context { DLogName() if (context == nil) { // Use default MOC context = self.managedObjectContext; } NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entity inManagedObjectContext:context]; NSFetchRequest *request = [[NSFetchRequest alloc] init]; [request setEntity:entityDescription]; if (predicate != nil) { [request setPredicate:predicate]; } if (sortDescriptors != nil) { [request setSortDescriptors:sortDescriptors]; } NSError *error = nil; NSArray *entities = [context executeFetchRequest:request error:&error]; if (entities == nil) { DLog(@"There was an error: %@", [error userInfo]); } return entities; } - (NSManagedObjectContext *)threadedManagedObjectContext { NSManagedObjectContext *threadedMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType]; threadedMoc.parentContext = self.managedObjectContext; return threadedMoc; } /** 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 *)managedObjectContext { if (_managedObjectContext != nil) { return _managedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { //_managedObjectContext = [[NSManagedObjectContext alloc] init]; _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [_managedObjectContext setPersistentStoreCoordinator:coordinator]; } return _managedObjectContext; } - (void)saveInManagedObjectContext:(NSManagedObjectContext *)context { if (context == nil) { // Use default MOC context = self.managedObjectContext; NSError *error = nil; if (context != nil) { if ([context hasChanges] && ![context save:&error]) { /* Replace this implementation with code to handle the error appropriately. abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. */ DLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } } } else { NSError *error = nil; // First save (child) context if ([context hasChanges] && ![context save:&error]) { /* Replace this implementation with code to handle the error appropriately. abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. */ DLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } // Then save parent context if ([self.managedObjectContext hasChanges]) { /* Replace this implementation with code to handle the error appropriately. abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. */ [self.managedObjectContext performBlock:^{ NSError *parentError = nil; [self.managedObjectContext save:&parentError]; if (parentError) { DLog(@"Unresolved error %@, %@", parentError, [parentError userInfo]); abort(); } }]; } } }
Core Data is designed to work in a multithreaded environment. However, not every object under the Core Data framework is thread safe. To use Core Data in a multithreaded environment, ensure that: Managed object contexts are bound to the thread (queue) that they are associated with upon initialization.
functions are thread safe and can be called from other threads. A NSManagedObject cannot be shared across threads. A NSManagedObjectID can be shared across threads. Changes from one NSManagedObjectContext can be merged into another NSManagedObjectContext , even on another thread.
Managed Object ContextThe NSManagedObjectContext class isn't thread safe. Plain and simple. You should never share managed object contexts between threads. This is a hard rule you shouldn't break.
From your perspective, the context is the central object in the Core Data stack. It's the object you use to create and fetch managed objects, and to manage undo and redo operations. Within a given context, there is at most one managed object to represent any given record in a persistent store.
Once all foreground threads have been stopped in a managed process (where the .exe file is a managed assembly), the system stops all background threads and shuts down. When the runtime stops a background thread because the process is shutting down, no exception is thrown in the thread.
A managed thread is either a background thread or a foreground thread. Background threads are identical to foreground threads with one exception: a background thread does not keep the managed execution environment running.
Use the Thread.IsBackground property to determine whether a thread is a background or a foreground thread, or to change its status. A thread can be changed to a background thread at any time by setting its IsBackground property to true.
Note: newly created Core Data objects have temporary object ids until the object is saved for the first time. Temporary object ids cannot be used to read objects on other threads.
Not totally sure if this applies to you but I was getting similar errors. I solved them by
Using NSPrivateQueueConcurrencyType
not NSConfinementConcurrencyType
to take processing off the main thread.
Putting executeFetchRequest
inside MOC's performBlockAndWait
.
So in CoreDataHelper.m's fetchEntity method you would have something like this:
[context performBlockAndWait:^{ NSError *error = nil; NSArray *entities = [context executeFetchRequest:request error:&error]; }];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With