So I am making a framework to hold all the code used to manipulate a core data database. What I have ended up having an issue with is this.
I have a method which returns a new item
- (NSManagedObject *)createItem;
once they have modified that item the would call
- (void)save;
This has to be able to be executed in multiple threads so the managed object I return from my method has to have an NSManagedObjectContext consistent inside a thread. My solution to solve this was to create a NSMutableDictionary to hold a references to NSManagedObjectContexts using
[NSThread hash]
as the key. This works great. The only problem is that I cannot get rid of the contexts once their threads have finished.
Does anyone have any idea on how I could detect that?
Here is the code for my managed object context method
// Return Managed Object Context for Framework
- (NSManagedObjectContext *)managedObjectContext
{
// Get Thread Hash Value
NSThread * currentThread = [NSThread currentThread];
NSNumber * hashValue = [NSNumber numberWithUnsignedInteger:[currentThread hash]];
// Get Context From Thread Hash
NSManagedObjectContext * context = [self.managedObjectContexts objectForKey:hashValue];
// Check Context Exists
if (!context)
{
// Create Managed Object Context With Persistent Store Coordinator In Main Thread
NSPersistentStoreCoordinator * coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:coordinator];
}
// Add Context To Available Contexts
[self.managedObjectContexts setObject:context forKey:hashValue];
}
// Return
return context;
}
Look at using the threadDictionary available on each NSThread. In this way you can easily get the MOC for a thread (or know if there isn't one) and the cleanup will be handled for you.
Related
I'm using Magical Record 2.3.0 beta 5 and I have troubles understanding how to get my NSManagedObjects for the current thread. I have a long running NSOperation where I need my PSPlayer (NSManagedObject).
When I init the NSOperation, I keep an id of my PSPlayer and re-fetch the same object in the operation's main method. According to Apple that the way to do it.
#implementation TAPlayerUpdateOperation
- (instancetype)initWithPlayer:(PSPlayer *)player;
{
self = [super init];
if (self) {
self.playerMD5Id = player.md5Id;
}
}
- (void)main
{
#autoreleasepool {
__block BOOL keepUpdating = YES;
PSPlayer *player = [[PSPlayer MR_findAllWithPredicate:[NSPredicate predicateWithFormat:#"md5Id == %#", self.playerMD5Id]] firstObject];
NSLog(#"player.md5Id = %#", player.md5Id);
// rest of my operation logic
}
}
#end
When I run my app with -com.apple.CoreData.ConcurrencyDebug 1, I get a crash when accessing the property in the NSLog statement.
What is the correct way to get my NSManagedObject so that it is safe for the current thread?
I've pinned the problem down to the following snippet where it crashes as well.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
PSPlayer *player =[[PSPlayer MR_findAll] firstObject];
NSLog(#"player = %#", player.name);
});
cheers,
Jan
You need to ensure that everything is saved and merged before the fetch would work. If you're using MR then it's better to take the managed object and call inContext: on it supplying the other context and have it do the work (it also avoids a predicate).
I expect the crash is because you use player.md5Id instead of self.playerMD5Id so you're accessinh the managed object on the wrong thread.
I have an old project (started on iOS 7) with this simple code:
in AppDelegate I create the managedObjectContext:
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
Then, I perform an update operation in a view controller:
[context performBlock:^{
[context deleteObject:anObject];
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Error saving context");
}
}];
I'm sure that this code was working correctly on iOS 7.0, but crashes on the performBlock: call on iOS 8 with this error:
Can only use -performBlockAndWait: on an NSManagedObjectContext that was created with a queue.
I can solve the error changing the init method of the NSManagedObjectContext instance like this:
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
But I don't understand why this behavior changed. In the documentation of NSManagedObjectContext you can read:
A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method.
So, in my first example, using a simple init call, the queue owner of the context is the main thread.
The performBlock: call was made on the main thread too, so I don't understand why this error. Am I missing something or it's a bug in iOS 8?
The call -[NSManagedObjectContext init] is just a wrapper for -[NSManagedObjectContext initWithConcurrencyType:] with the argument NSConfinementConcurrencyType. This creates an instance of NSManagedObjectContext that uses the obsolete thread confinement model - which does not uses a queue. Contexts created using init or initWithConcurrencyType: with the value NSConfinementConcurrencyType passed are not compatible with the queue methods performBlock: or performBlockAndWait:.
Create your context using -[NSManagedObjectContext initWithConcurrencyType:] and pass either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType as appropriate. The context that is created as a result is compatible with performBlock: and performBlockAndWait: and will uses the queue confinement model that was introduced in iOS 5.
How do you create a NSManagedObjectContext that has a non nil parentContext that runs on a different queue/thread?
UIManagedDocument's managedObjectContext does have this, but I don't know how to replicate it without using UIManagedDocument.
This is the code I'm using, which results in a managedObjectContext whose parentContext property is nil.
-(NSManagedObjectContext *)context{
if (_context == nil){
_context = [[NSManagedObjectContext alloc] init];
_context.persistentStoreCoordinator = self.storeCoordinator;
}
return _context;
}
-(NSPersistentStoreCoordinator *) storeCoordinator{
if (_storeCoordinator == nil) {
_storeCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model];
NSError *err = nil;
if (![_storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.dbURL
options:nil
error:&err ]) {
NSLog(#"Error while adding a Store: %#", err);
return nil;
}
}
return _storeCoordinator;
}
-(NSManagedObjectModel *) model{
if (_model == nil) {
_model = [[NSManagedObjectModel alloc] initWithContentsOfURL:self.modelURL];
}
return _model;
}
You create the child context by just allocating it and setting its parent:
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] init];
[childContext setParentContext:[self managedObjectContext]];
The persistent store coordinator is inherited from the parent context, so this is all you need to create the child. But the parent context must use one of the queue-based concurrency types. That means that your code above creating the context would have to change to something like:
_context = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSMainQueueConcurrencyType];
There's also NSPrivateQueueConcurrencyType. Which is better depends on your app's design.
The runs on a different queue/thread thing is a different matter though. This is never automatic. Your code runs on whatever queue or thread you call it from. Core Data's direct support is limited to using one of the queue-based concurrency types-- but then you need to make sure to use performBlock: or performBlockAndWait: to ensure that the context's operations actually happen on the right queue.
After creating the NSManagedObjectContext you have to assign the parent context if you need it to be a child context.
[newContext setParentContext:mainThreadManagedObjectContext];
In this case you do not even have to assign the persistent store. From the docs:
Rather than specifying a persistent store coordinator for a managed object context, you can now specify a parent managed object context using setParentContext:. This means that fetch and save operations are mediated by the parent context instead of a coordinator.
Is it possible and practical to create a Core Data class method that will return the current instance of managedObjectContext? I am wondering so that I can segue to other controllers and load modal views without having to pass the managedObjectContext.
Also if I am using Core Data with dispatch_async I know I need to create my own instance of managedObjectContext but I can use the same coordinator. Will this make the information accessible both inside the dispatch_async and in the main thread?
I am basically using the dispatch_async to get data from the API and store it while the user is using the application.
In the past, I've created a Core Data manager singleton class that has simplified things. Here is an example, but this is pre-iOS5/ARC, so some changes need to be made.
I had a similar issue when trying to asynchronously getting data from my server to the app. My method is a bit different, but basically here it is (this is a 4.3 project, so no ARC):
The following methods are in my DataUpdater singleton. This first method is called at app startup:
- (void) update { //download the updates on a new thread
[NSThread detachNewThreadSelector:#selector(updateThread)
toTarget:self withObject:nil];
}
It initializes a thread with this selector, which is responsible only for downloading the content from the API, then passing it back to the main thread to be saved.
- (void) updateThread { //the actual update thread
//New thread, new auto-release pool
//(dunno if you need to do anything fancy for ARC)
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
//...
//YOUR CODE TO DOWNLOAD (BUT *NOT* SAVE) DATA FROM THE SERVER
//DON'T CREATE ANY MANAGED OBJECTS HERE
//...
//Pass the data to the main thread to perform
//the commit to the Core Data Model
[self performSelectorOnMainThread:#selector(saveUpdate:)
withObject:data waitUntilDone:NO];
//kill the thread & the auto-release pool
[NSThread exit];
[pool release];
}
Now that we're back on the main thread, the data is added to the Core Data Model and then the context is saved.
- (void) saveUpdate:(NSArray *) data {
//add the objects to your Core Data Model
//and save context
NSError * error = nil;
[[[CoreManager defaultCoreManager] CoreContext] save:&error];
if (error) {
[NSException raise:#"Unable to save data update"
format:#"Reason: %#", [error localizedDescription]];
} else {
[[NSNotificationCenter defaultCenter] postNotification:
[NSNotification notificationWithName:#"DONE" object:nil]];
}
}
Dealing with the first part of the question only (you shouldnt really ask multiple questions!) you don't have to pass the managed object context around - presumably you are passing a managed object? In that case the context is available as a property of the managed object itself - .managedObjectContext.
I'm contemplating how to offload the drawing of a very large Core Data tree structure to CATiledLayer. CATiledLayer seems to be awesome because it performs drawing on a background thread and then fades in tiles whenever they're drawn. However, because the information of the drawing comes from a Core Data context that is by design not thread safe, I'm running into race condition issues where the drawing code needs to access the CD context.
Normally, if I need to perform background tasks with Core Data, I create a new context in the background thread and reuse the existing model and persistent store coordinator, to prevent threading issues. But the CATiledLayer does all the threading internally, so I don't know when to create the context, and there needs to be some kind of context sharing, or I can't pass the right entities to the CATiledLayer to begin with.
Is there anyone with a suggestion how I can deal with this scenario?
Cheers,
Eric-Paul.
The easiest solution is to use the dispatch API to lock all of your data access onto a single thread, while still allowing the actual drawing to be multi-threaded.
If your existing managed object context can only be accessed on the main thread, then this is what you do:
- (void)drawInContext:(CGContextRef)context // I'm using a CATiledLayer subclass. You might be using a layer delegate instead
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObject *record = self.managedObjecToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
This is a quick and easy solution, but it will lock your main thread while fetching the data, which is almost certainly going to create "hitches" whenever a new tile is loaded while scrolling around. To solve this, you need to perform all of your data access on a "serial" dispatch queue.
The queue needs to have it's own managed object context, and you need to keep this context in sync with the context on your main thread, which is (presumably) being updated by user actions. The easiest way to do this is to observe a notification that the context has changed, and throw out the one used for drawing.
Define an instance variable for the queue:
#interface MyClass
{
NSManagedObjectContext *layerDataAccessContext;
dispatch_queue_t layerDataAccessQueue;
}
#end
Create it in your init method:
- (id)init
{
layerDataAccessQueue = dispatch_queue_create("layer data access queue", DISPATCH_QUEUE_SERIAL);
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextDidChange:) name:NSManagedObjectContextDidSaveNotification object:nil]; // you might want to create your own notification here, which is only sent when data that's actually being drawn has changed
}
- (void)contextDidChange:(NSNotification *)notif
{
dispatch_sync(layerDataAccessQueue, ^{
[layerDataAccessContext release];
layerDataAccessContext = nil;
});
[self.layer setNeedsDisplay];
}
And access the context while drawing:
- (void)drawInContext:(CGContextRef)context
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(layerDataAccessQueue, ^{
NSManagedObject record = self.managedObjectToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
- (NSManagedObject *)managedObjectToDraw
{
if (!layerDataAccessContext) {
__block NSPersistentStoreCoordinator *coordinator;
dispatch_sync(dispatch_get_main_queue(), ^{
coordinator = [self persistentStoreCoordinator];
});
layerDataAccessContext = [[NSManagedObjectContext alloc] init];
[layerDataAccessContext setPersistentStoreCoordinator:coordinator];
}
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Employee"
inManagedObjectContext:layerDataAccessContext];
[request setEntity:entity];
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"self == %#", targetObject];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *array = [layerDataAccessContext executeFetchRequest:request error:&error];
NSManagedObject *record;
if (array == nil || array.count == 0) {
// Deal with error.
}
return [array objectAtIndex:0];
}
I've given up trying to share managed object context instances between CATiledLayer draws and now just alloc/init a new context at every call of drawLayer:inContext: The performance hit is not noticable, for the drawing is already asynchronous.
If there's anyone out there with a better solution, please share!