1

I have some code that takes a little time to process and thus appropriately it should not run on the main queue. However I'm not sure on how to correctly "structure" the GCD code segments. I.e every time the app becomes active I'm doing a syncing operation:

AppDelegate.m

- (void)applicationDidBecomeActive:(UIApplication *)application {

    AddressBookHelper *abHelper = [AddressBookHelper sharedInstance]; // singleton helper class of NSObject

    [abHelper sync];
}

The syncing code inside AddressBookHelper looks something like this:

AddressBookHelper.m

- (void)sync {

    NSArray *people = // Fetching some people from Core Data

    NSMutableArray *syncConflicts;
    // Start doing some business logic, iterating over data and so on

    for (id object in people) {
    // Process some data
        [syncConflicts addObject:object];
    }

    self.syncConflicts = syncConflicts;

    // I have separated this method to keep the code cleaner and to separate the logic of the methods
    [self processSyncConflicts];
}

- (void)processSyncConflicts {

    if ([self.syncConflicts count] > 0) {
        // Alert the user about the sync conflict by showing a UIAlertView to take action
        UIAlertView *alert;
        [alert show];

    } else {

        // Syncing is complete
    }
}

So with this code structure, how would I properly use GCD to put this code on a background thread?

Is it as easy as doing this?

AppDelegate.m

- (void)applicationDidBecomeActive:(UIApplication *)application {

    AddressBookHelper *abHelper = [AddressBookHelper sharedInstance]; // singleton helper class of NSObject

    dispatch_queue_t queue = dispatch_queue_create("addressbookSyncQueue", 0);
    dispatch_async(queue, ^{
            [abHelper sync];
    });
}

AddressBookHelper.m

- (void)processSyncConflicts {

    if ([self.syncConflicts count] > 0) {
        // Alert the user about the sync conflict by showing a UIAlertView to take action
        UIAlertView *alert;
        dispatch_queue_t mainQueue = dispatch_get_main_queue();

        dispatch_async(mainQueue, ^{
            [alert show];
        });

    } else {

        // Syncing is complete
    }
}
Peter Warbo
  • 11,136
  • 14
  • 98
  • 193

1 Answers1

7

I see several potential problems in your code.

Multiple Simultaneous Syncs

Suppose you receive applicationDidBecomeActive:, but before [abHelper sync] finishes, the user switches away from your app, and then back to your app. Now you receive another applicationDidBecomeActive:, create a new GCD queue, and start another [abHelper sync] while the first one is still running. Bad!

Change your AddressBookHelper class to create its own GCD queue and store it in an instance variable. Change the interface so that the sync operation puts itself on that queue. Example:

@interface AddressBookHelper : NSObject

+ (AddressBookHelper *)sharedInstance;

- (void)syncInBackground;

@end

@implementation AddressBookHelper {
    dispatch_queue_t queue_;
}

- (void)syncInBackground {
    if (!queue_) {
        queue_ = dispatch_queue_create("AddressBookHelper", 0);
    }
    dispatch_async(queue_, ^{ [self syncOnCurrentQueue]; });
}

- (void)syncOnCurrentQueue {
    // This is now a private method that always runs on queue_.
    // ...
}

@end

Property Thread-Safety

I assume your main thread needs to access syncConflicts, since you're storing it in a property. If so, you should update syncConflicts on the main queue, so that you don't change it in the middle of some main-queue operation that is using it.

    dispatch_sync(dispatch_get_main_queue(), ^{
        self.syncConflicts = syncConflicts;
    });

Core Data Thread-Safety

You didn't show us how you access Core Data. You need to be aware that Core Data objects are not generally thread-safe. Your background sync operation should create a managed object context of its own, as a child of the main managed object context. Then, when the operation is complete, it can tell the child context to save, and the changes will be pushed to the parent context in a thread-safe way.

Since you'll be getting your people objects from this child context, you can't pass them back to the main thread. Instead, you need to store each object's objectID, and use that to recreate the objects using the main thread's context.

- (void)syncOnCurrentQueue {
    // This is now a private method that always runs on queue_.

    NSManagedObjectContext *syncContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
    syncContext.parentContext = self.mainContext;

    NSArray *people = nil; // Fetching some people from syncContext

    NSMutableArray *syncConflictIDs = [[NSMutableArray alloc] init];
    // Start doing some business logic, iterating over data and so on

    for (id object in people) {
        // Process some data
        [syncConflictIDs addObject:[object objectID]];
    }

    NSError *error;
    if (![syncContext save:&error]) {
        // save to main context failed...
    }

    dispatch_sync(dispatch_get_main_queue(), ^{
        // assuming main context has NSMainQueueConcurrencyType
        NSMutableArray *syncConflicts = [[NSMutableArray alloc] initWithCapacity:syncConflictIDs.count];
        for (NSManagedObjectID *objectID in syncConflictIDs) {
            [syncConflicts addObject:[self.mainContext objectWithID:objectID]];
        }
        self.syncConflicts = syncConflicts;
    });

    // I have separated this method to keep the code cleaner and to separate the logic of the methods
    [self processSyncConflicts];
}

UIKit Thread-Safety

You can only manipulate UIKit objects like UIAlertView on the main thread. You didn't actually show where you alloc/init your alert view, but I assume you're not forcing it to be on the main thread given where you declared the variable. You need to make sure you do it on the main thread.

- (void)processSyncConflicts {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([self.syncConflicts count] > 0) {
            // Alert the user about the sync conflict by showing a UIAlertView to take action
            UIAlertView *alert = [[UIAlertView alloc] init...];
            [alert show];
        } else {
            // Syncing is complete
        }
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thanks! Super thorough answer! I'm implementing your solutions and a couple of questions arose in regards to your answer. I'll get back to you with them in a while. – Peter Warbo Dec 05 '12 at 15:03