How to correctly handle threading when drawing Core Data entity information with CATiledLayer

685 views Asked by At

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.

2

There are 2 answers

0
epologee On BEST ANSWER

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!

0
Abhi Beckert On

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];
}