The standard way of using multithread core-data is
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
However, the one have external managed object that save immediately after another. This creates errors. For example:
- (void)mergeChanges:(NSNotification *)notification
{
NSManagedObjectContext *mainContext = [self managedObjectContext];
// Merge changes into the main context on the main thread
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
- (void) loadingIntoCoreData
{
NSManagedObjectContext *ctx = [[NSManagedObjectContext alloc] init];
[ctx setUndoManager:nil];
[ctx setPersistentStoreCoordinator: [self persistentStoreCoordinator]];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:ctx];
...
// Create many objects
[ctx save:&error];
[self doSomethingWithThisCtx:ctx];
}
- (void) doSomethingWithThisCtx:(NSManagedObjectContext *)ctx{
// Form relationships with objects create in - (void) loadingIntoCoreData
[ctx save:&error];
}
Then on the second [ctx save:&error] will throw an "EXC_BAD_ACCESS" error.
How would one insert into core data and create relationships with the new objects on a separate thread? It works fine on the main thread but since it's done during applicationDidLaunched, then the UI get locked up.
You usually don't call the main thread context directly. Instead, you register the object that holds the main thread context for the NSManagedObjectContextDidSaveNotification of the context on the background thread. Upon recent of the notification, call the main thread context's NSManagedObjectContextDidSaveNotification: and pass it the notification object. The main thread will then update itself.
This makes sure that the main thread is updated before it tries to use any of the objects added or deleted on the background thread.
Related
I have a parent - child - grandchild core data context setup in Core Data as below. Whenever I try execute a fetch request on the grandchild context, it causes a deadlock on the thread
- (NSManagedObjectContext *)defaultPrivateQueueContext
{
if (!_defaultPrivateQueueContext) {
_defaultPrivateQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_defaultPrivateQueueContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
}
return _defaultPrivateQueueContext;
}
- (NSManagedObjectContext *)mainThreadContext {
if (!_mainThreadContext) {
_mainThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainThreadContext.parentContext = [self defaultPrivateQueueContext];
}
return _mainThreadContext;
}
+ (NSManagedObjectContext *)newPrivateQueueContext
{
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.parentContext = [[self sharedParliamentAPI] mainThreadContext];
return context;
}
This is my code where it causes a deadlock (when trying to execute a fetch request):
- (void)fetchMenuItemsWithCompletion:(void (^) (BOOL success, NSString *message))completionBlock {
NSMutableURLRequest *request = [APIHelper createNewRequestWithURLExtension:#"menuitems" httpMethodType:#"GET" parameters:nil];
NSURLSession *session = [NSURLSession sessionWithConfiguration:self.sessionConfig];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSObject *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if ([[json valueForKey:#"isSuccess"] boolValue]) {
NSManagedObjectContext *defaultContext = self.defaultPrivateQueueContext;
NSManagedObjectContext *privateQueueContext = [ParliamentAPI newPrivateQueueContext];
[privateQueueContext performBlock:^{
__block NSError *error;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MenuItem"];
NSArray *fetchedRecords = [privateQueueContext executeFetchRequest:request error:&error];
// do stuff with fetchedRecords
}];
} else {
completionBlock([[json valueForKey:#"isSuccess"] boolValue], [json valueForKey:#"message"]);
}
}];
[dataTask resume];
}
Your Core Data object structure looks like this:
Where you have a NSPrivateQueueConcurrencyType context as the root context connected to the persistent store coordinator, which has a child NSMainQueueConcurrencyType context, which in turn has a NSPrivateQueueConcurrencyType context. This structure is recommended by a lot of people who write on The IntarWebs.
What is happening in your case is that the private queue context that is the main queue context is becoming busy, which is causing it's child to wait. Because the main queue context is using the main queue for all of it's work, it's not necessarily busy doing Core Data work when this happens (though this is still somewhat likely). The main queue does a lot of work other than Core Data, and all of those things will end up impacting the child any time it needs to communicate with the main queue context.
Additionally, a context created with NSMainQueueConcurrencyType is allowed to execute Core Data operations without explicitly using performBlock: or performBlockAndWait:.
For example, a main queue context can do this:
NSArray *results = nil;
NSError *error = nil;
results = [mainQueueContext executeFetchRequest:fetchRequest error:&error];
And is not required to do this:
[mainQueueContext performBlock:^{
NSArray *results = nil;
NSError *error = nil;
results = [mainQueueContext executeFetchRequest:fetchRequest error:&error];
}];
The difference here is that the first example without the performBlock: will block the main thread waiting for the result. The second, using performBlock:, is asynchronous and will not block - the fetch request will be scheduled for execution on the queue. Obviously, the first example can cause some contention in any child contexts.
If your configuration had a NSMainQueueConcurrencyType context that was a child of another NSMainQueueConcurrencyType context, that would be... bad. It's almost guaranteed to deadlock in that configuration. The good news is, you don't have that issue!
You can convert your code to using performBlock: with the NSMainQueueConcurrencyType context to mitigate this part of the problem. You can also use an NSPrivateQueueConcurrencyType in place of your main queue context - there is not much of a good reason to use a main queue context at all. NSFetchedResultsController can be used with a private queue context to do "background fetching".
I have an NSOperationQueue that has some NSBlockOperations added to it, among which are blockOperations A and B. The NSOperationQueue has a maxConcurrencyOperationCount of 1.
blockOperation B, is dependant on A being finished. In each of the blockOperations I am calling a method, which in turn calls another method, that initialises a new NSManagedObjectContext (with the persistentStoreCoordinator from a singleton), that I use to create and add objects to a Core Data database. The code invoked by the aforementioned second method call in A and B looks like this (it varies slightly for for each of them):
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = [[CoreDataController sharedCoreDataController] persistantStoreCoordinator];
for (NSDictionary *articleDictionary in array) {
if (![Article articleExistsWithIDInDictionary:articleDictionary inContext:managedObjectContext]) {
[Article articleFromDictionary:articleDictionary inContext:managedObjectContext];
}
}
[[CoreDataController sharedCoreDataController] saveContext:managedObjectContext];
// method ends.
The saveContext: code looks like this:
NSError *error = nil;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
Having spent a lot of time reading Apples Docs about Core Data Concurrency, NSOperation, etc., I'm still unsure if what I'm doing with NSManagedObjectContext is thread-safe, and generally considered to be OK? Some clarification and/or indication of that I should be doing differently would be much appreciated. If you need to see any more code, please ask.
Thanks in advance.
What you are doing is NOT thread safe.
If you decide to create a context per operation, you better use the confinement concurrency type:
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
this way you don't need to change anything in your current code.
if you want to use the context with NSPrivateQueueConcurrencyType you must access objects within that context by the performBlock: or performBlockAndWait::
[managedObjectContext performBlockAndWait:^{//wait is used as to not end the operation before this code is executed
for (NSDictionary *articleDictionary in array) {
if (![Article articleExistsWithIDInDictionary:articleDictionary
inContext:managedObjectContext])
{
[Article articleFromDictionary:articleDictionary
inContext:managedObjectContext];
}
}
}];
I would probably go with my first solution in your case.
all that said, you could simply use a "private queue" context as a serial queue (as long as you add block operation to it in the order you need them to execute).
A context performBlock: method will queue the block and execute it serially in regard to other blocks added that context for execution in the background:
//add this to your CoreDataController
context = [[CoreDataController sharedCoreDataController] serialExecutionBGContext];
[context performBlock:^{ //your block operation code1}];
[context performBlock:^{ //your block operation code2}];
this will perform code1 and code2 in the background serially.
In this manner, you save the overhead of allocating a new context, and might benefit from caching done by this context.
You might want to reset this context every now and then so it will not get bloated with fetched objects.
The concern with the context is that it be accessed only within a single thread. Setting the MaxConcurrencyOperationCount does not guarantee that. Another approach is to make the context a "thread" variable, storing a context in each thread dictionary where it is used.
Ex:
+ (NSManagedObjectContext*)managedObjectContext
{
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSManagedObjectContext *context = [threadDictionary valueForKey:#"QpyManagedObjectContext"];
if (context == nil) {
#autoreleasepool {
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[context setStalenessInterval: 10.0];
[context setMergePolicy:[[NSMergePolicy alloc]initWithMergeType:NSOverwriteMergePolicyType]];
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[Qpyd managedObjectModel]];
[context setPersistentStoreCoordinator:coordinator];
NSString *STORE_TYPE = NSSQLiteStoreType;
NSString *path = [[NSProcessInfo processInfo] arguments][0];
path = [path stringByDeletingPathExtension];
NSURL *url = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:#"sqlite"]];
NSError *error;
NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE configuration:nil URL:url options:nil error:&error];
if (newStore == nil) {
NSLog(#"Store Configuration Failure %#", ([error localizedDescription] != nil) ? [error localizedDescription] : #"Unknown Error");
}
[threadDictionary setObject:context forKey:#"QpyManagedObjectContext"];
}
}
return context;
}
A typical setup: we have a main thread with a mainMOC and a background thread with its own backgroundMOC. The background thread performs read-only operations on the backgroundMOC by dispatching blocks to a backgroundQueue.
The backgroundMOC needs to merge the changes from the mainMOC so we register for NSManagedObjectContextDidSaveNotification and then do something like
- (void)mainMocDidSave:(NSNotification *)notification {
dispatch_async(backgroundQueue, ^{
[backgroundMoc mergeChangesFromContextDidSaveNotification:notification];
});
}
Let's say the user deletes an object in the mainMOC. The code above does not seem safe to me, since the merge will be done at some point in the future. Until the merge is done, there might still be blocks on the backgroundQueue that are trying to use the deleted object.
The obvious solution would be to use dispatch_sync instead (or performBlockAndWait, performSelector:OnThread:...) instead. From the code snippets I see on the interwebs, this seems to be what everybody is doing. But I'm not comfortable with this solution either.
The name NSManagedObjectContextDidSaveNotification implies that the save has already happened when the notification is delivered. So the corresponding row has already been deleted from the underlying database (assuming an sqlite store). dispatch_sync will have to wait for other blocks on the queue to finish before it can merge the changes, and these other blocks could still try to work with the deleted object, leading to an NSObjectInaccessibleException.
It seems to me that the correct way to merge changes from one thread/queue to another would be to
Subscribe to NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification on the background thread.
On NSManagedObjectContextWillSaveNotification: empty the backgroundQueue and suspend any operations that dispatch new blocks to the queue.
On NSManagedObjectContextDidSaveNotification: merge the changes synchronously.
Resume normal operation on the background queue.
Is this the correct approach or am I missing something?
I use the following structure in two projects in which I experienced similar troubles as you did. First of all I use singleton service to ensure there is only one background thread merging and reading changes.
AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
// It is crucial to use the correct concurrency type!
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
- (void)saveContext {
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext 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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"ParentContextDidSaveNotification" object:nil];
}
}
}
BackgroundService.m
- (id)init {
self = [super init];
if (self) {
[self managedObjectContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(parentContextDidSave) name:#"ParentContextDidSaveNotification" object:nil];
}
return self;
}
- (NSManagedObjectContext *)managedObjectContext {
if (!_managedObjectContext) {
// Again, make sure you use the correct concurrency type!
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_managedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
}
return _managedObjectContext;
}
- (BOOL)saveContext {
#synchronized(self) {
BOOL successful = YES;
// Bad practice, process errors appropriately.
[[self managedObjectContext] save:nil];
[[[self managedObjectContext] parentContext] performBlock:^{
[(AppDelegate *)[[UIApplication sharedApplication] delegate] saveContext];
}];
return successful;
}
}
- (void)parentContextDidSave {
[[self managedObjectContext] reset];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ManagedObjectContextResetNotification" object:nil];
}
The Title is the whole question. If the _asych block of code produces meaningful work it will in some cases have produced information which the main thread would now like to use.
In this bare example, how would you get the data value, the string data, contained in myData out of the block for the main thread to work with:
dispatch_queue_t myQueue = dispatch_queue_create("com.mycompany.myqueue", 0);
dispatch_async(myQueue, ^{
NSString *myData = [self getSavedData];
});
dispatch_async(myQueue, ^{ dispatch_release(myQueue); });
Please extend the code help to show me, in simple usage, where and how this NSLog, or its correct equivalent, would be placed in the main thread of the program relative to the GCD block:
NSLog(#"%#", myData);
You can nest blocks, yet have each run in different threads.
dispatch_queue_t myQueue = dispatch_queue_create("someid", 0);
dispatch_async(myQueue, ^{
NSString *myData = [self getSavedData];
dispatch_async(dispatch_get_main_queue(), ^{
self.someLabel.text = myData;
});
});
dispatch_async(myQueue, ^{ dispatch_release(myQueue); });
If your code is long, it's unwieldy to have in nested blocks. So simply call a method inside dispatch_async like [self processData:myData].
I have got this background thread that does a few things with core data objects. I get the context as follows:
- (id)_managedObjectContextForThread;
{
NSManagedObjectContext * newContext = [[[NSThread currentThread] threadDictionary] valueForKey:#"managedObjectContext"];
if(newContext) return newContext;
newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:[[[NSApplication sharedApplication] delegate] persistentStoreCoordinator]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(_mergeChangesFromManagedObjectContext:)
name:NSManagedObjectContextDidSaveNotification
object:newContext];
[[[NSThread currentThread] threadDictionary] setValue:newContext forKey:#"managedObjectContext"];
return newContext;
}
then I fetch some objects, modify them and save the context:
- (void) saveContext:(NSManagedObjectContext*)context {
NSError *error = nil;
if (![context save:&error]) {
[[NSApplication sharedApplication] presentError:error];
}
}
- (void)_mergeChangesFromManagedObjectContext:(NSNotification*)notification;
{
[[[[NSApplication sharedApplication] delegate] managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
.. later I remove the observer. This works for the main part. But some properties don't get updated when they get merged back. The properties that were nil before get updated. The ones that had a value stay the same.
I tried:
[newContext setMergePolicy:NSOverwriteMergePolicy];
... (and the other merge policies) on the main context but it did not work :P
Thank you for your help.
Note: I have bound the values to a NSTableView. I log them after the merge. The values properties that were nil seem to work fine.
How are you registering both contexts for notifications? You need to do something like this:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundMOC];
[nc addObserver:self
selector:#selector(mainContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:mainMOC];
And implement the callbacks:
// merge changes in background thread if main context changes
- (void)mainContextDidSave:(NSNotification *)notification
{
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[backgroundMOC performSelector:selector onThread:background_thread withObject:notification waitUntilDone:NO];
}
// merge changes in main thread if background context changes
- (void)backgroundContextDidSave:(NSNotification *)notification
{
if ([NSThread isMainThread]) {
[mainMOC mergeChangesFromContextDidSaveNotification:notification];
}
else {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:) withObject:notification waitUntilDone:NO];
}
}