Question

I need to take a snapshot of a mapView with the MKMapSnapshotter API and do some manipulation of the image it returns, as well as fetching and writing to core data. As it's all one piece of work, I wrapped it up in an NSOperation object which is executed on a background NSOperationQueue.

The MKSnapshotter api either runs the completion block on the main thread:

- (void)startWithCompletionHandler:(MKMapSnapshotCompletionHandler)completionHandler

or it lets you specify a dispatch queue to use:

- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MKMapSnapshotCompletionHandler)completionHandler

...so when it returns in setSnapshot: I'm always on a different thread than I should be, which potentially borks my core data managed object context. Is there a way to refer to the thread I called the snapshotter from? Or do I need to rethink my design here?

- (instancetype) initWithManagedObject:(NSManagedObject *)managedObject {

    // store object id for later use
    _objectID = managedObject.objectID;
    _mainThreadContext = managedObject.managedObjectContext;
    ...
}

- (void)start {
    // do stuff with core data on the background moc
    self.backgroundContext = [[NSManagedObjectContext alloc] init];
    [self.backgroundContext setParentContext:self.parentContext];
    NSError *objectIDRetrievalError = nil;
    self.objectInBackgroundContext = [self.backgroundContext existingObjectWithID:self.objectID error:&objectIDRetrievalError];
    ...

    // take the map snapshot
    ...
    [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
        ...
        self.snapshot = snapshot;
    }];
}
- (void)setSnapshot:(MKMapSnapshot *)snapshot {     
    // HELP, I'm on the wrong thread! 

    // do some expensive image stuff
    ...
    // do some more stuff with core data on the background moc
    [self.objectInBackgroundContext doStuff];
    ...
    [self.backgroundContext save:&error];
    ...
    [self finish];
}
Was it helpful?

Solution

If you are executing the NSOperation concurrently, as your code indicates, you basically have no control which thread is used to execute the operation's start method.

This complicates the latter.

Thus, I would suggest to create your backgroundContext using a NSPrivateQueueConcurrencyType concurrency type, like so:

self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

(see NSManagedObjectContext: Concurrency).

This enables you to execute any code within the managed object context's appropriate "execution context":

Invoking it asynchronously:

[self.backgroundContext performBlock: ^{
    // executing on the background context's private queue
    ...
}];

Invoking it synchronously:

[self.backgroundContext performBlockAndWait: ^{
    // executing on the background context's private queue
    ...
}];

Then, in your start method you invoke the "snap shotter" as follows:

...
[snapshotter startWithQueue:dispatch_get_global_queue(0, 0) 
          completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
    [self.backgroundContext performBlock:^{
        // executing on the background context's private queue
        ...
        self.snapshot = snapshot;

        // here you likely need to orderly "terminate" the operation queue:
        dispatch_async(_syncQueue, ^{
            // set operation result:
            if (error) { ... }
            [self terminate];
        }
    }];
}];

Side note:

The implementation of terminate may look as follows:

- (void) terminate {
    self.isExecuting = NO;
    self.isFinished = YES;
    completion_block_t completionHandler = _completionHandler;
    _completionHandler = nil;
    id result = _result;
    _self = nil;
    if (completionHandler) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            completionHandler(result);
        });
    }
}

OTHER TIPS

You can use a dispatch semaphore to wait for the completion handler to run and then assign your snapshot property after it has finished, in the original context. It's not great to block the thread in an operation, but given the constraints of the API, it may be the best choice.

- (void)start {
    // do stuff with core data on the background moc
    self.backgroundContext = [[NSManagedObjectContext alloc] init];
    [self.backgroundContext setParentContext:self.parentContext];
    NSError *objectIDRetrievalError = nil;
    self.objectInBackgroundContext = [self.backgroundContext existingObjectWithID:self.objectID error:&objectIDRetrievalError];
    ...

    // take the map snapshot
    ...
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    __block MKMapSnapshot *result;
    [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
        ...
        result = snapshot;
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    self.snapshot = result;
}

If you just want to call setSnapshot on main thread you can accomplish this by submitting operation to queue associated with main thread: [NSOperationQueue mainQueue]

NSOperation *op = [NSBlockOperation blockOperationWithBlock:^{self.snapshot = snapshot;}];
[[NSOperationQueue mainQueue] addOperation:op]

Or less verbose version with GCD

dispatch_async(dispatch_get_main_queue(), ^{
    self.snapshot = snapshot;
});

Update v2

You may find helpful coredata's builtin concurrency support. You may start from Concurrency Support for Managed Object Contexts

- (void)start {
    // do stuff with core data on the background moc
    self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [self.backgroundContext setParentContext:self.parentContext];
    __block NSError *objectIDRetrievalError = nil;
    [self.backgroundContext performBlockAndWait:^{
        self.objectInBackgroundContext = [self.backgroundContext existingObjectWithID:self.objectID error:&objectIDRetrievalError];
    }];
    ...

    // take the map snapshot
    ...
    [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
        ...
        self.snapshot = snapshot;
    }];
}
- (void)setSnapshot:(MKMapSnapshot *)snapshot {     
    // HELP, I'm on the wrong thread! 

    // do some expensive image stuff
    ...
    [self.backgroundContext performBlockAndWait:^{
        // do some more stuff with core data on the background moc
        [self.objectInBackgroundContext doStuff];
        ...
        [self.backgroundContext save:&error];
    }];
    ...
    [self finish];
}

Update v3

As @CouchDeveloper pointed out current operation implementation is not quite correct. According to Concurrency Programming Guide and NSOperation reference the start method should

  1. check whether the operation was cancelled
  2. establish execution environment for the task (basically create the thread or queue)
  3. if main implemenhted
    1. call main method on context from (2)
    2. else run operation itself inside start on context from (2)

Then, combining all the stuff:

- (void)start {
    // Always check for cancellation before launching the task.
    if ([self isCancelled])
    {
        // Must move the operation to the finished state if it is canceled.
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    // If the operation is not canceled, begin executing the task.
    [self willChangeValueForKey:@"isExecuting"];
    // Run main in concurrent queue
    self.dispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(self.dispatchQueue, ^{
        [self main]
    });
    executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
    // it runs inside self.dispatchQueue queue
    // create moc with its own private queue
    self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [self.backgroundContext setParentContext:self.parentContext];
    __block NSError *objectIDRetrievalError = nil;
    [self.backgroundContext performBlockAndWait:^{
        self.objectInBackgroundContext = [self.backgroundContext existingObjectWithID:self.objectID error:&objectIDRetrievalError];

    }];
    ...
    // take the map snapshot
    ...
    [snapshotter startWithQueue:self.dispatchQueue // make the completion handler run in self.dispatchQueue
              completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
                  ...
                  self.snapshot = snapshot;
              }];
}
- (void)setSnapshot:(MKMapSnapshot *)snapshot {
    // will be performed in self.dispatchQueue too
    // do some more stuff with core data on the background moc
    [self.backgroundContext performBlockAndWait:^{
        [self.objectInBackgroundContext doStuff];
        ...
        [self.backgroundContext save:&error];

    }];
    ...
    [self finish];
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top