1

I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.

Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.

For example:

@implementation aViewController
- (void) viewDidLoad {
    [self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
        if( err ) NSLog( @"There was a problem opening the database" );
    }];
}

- (IBAction) goButtonTouched: (id) sender {
    // Wait here until the database is open and ready to use.
    if( ???DatabaseNotAvailableYet??? ) {
        [self putSpinnerOnScreen];
        ???BlockProgressHereUntilDatabaseAvailable???
        [self takeSpinnerOffScreen];
    }
    // Use the database...
    NSManagedObjectContext *context = [self theDatabaseContext];

    // Build the search request for the attribute desired
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
    request.predicate = [NSPredicate predicateWithFormat: @"dId == %@", sender.tag];
    request.sortDescriptors = nil;

    // Perform the search
    NSError *error = nil;
    NSArray *matches = [context executeFetchRequest: request error: &error];

    // Use the search results
    if( !matches || matches.count < 1 ) {
        NSLog( @"Uh oh, got a nil back from my Destination fetch request!" );
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"No Info"
                    message: @"The database did not have information for this selection"
                   delegate: nil
          cancelButtonTitle: @"OK"
          otherButtonTitles: nil];
        [alert show];
    } else {
        MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
        movc.destDetails = [matches lastObject];
        [self.navigationController pushViewController: movc animated: YES];
    }
}
@end

My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.

I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.

I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.

I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.

I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.

This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.

LavaSlider
  • 2,494
  • 18
  • 29

4 Answers4

1

I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.

The following should work:

@implementation aViewController
- (void) viewDidLoad {
    self.waitingForDB = NO;
    self.databaseReady = NO;
    [self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
            if( err ){ 
                NSLog( @"There was a problem opening the database" )
            }else{
                [self performSelectorOnMainThread:@selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
            };
    }];
}

- (void)handleDatabaseReady{
    self.databaseReady = YES;
    if(self.waitingForDB){
        [self takeSpinnerOffScreen];
        [self go];
    }

}

- (IBAction) goButtonTouched: (id) sender {
    // Wait here until the database is open and ready to use.
    if( !self.databaseReady ) {
        self.waitingForDB = YES;
        [self putSpinnerOnScreen];
    else{
        [self go];
    }
}

-(void)go{
    // Use the database...
    NSManagedObjectContext *context = [self theDatabaseContext];

    // Build the search request for the attribute desired
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
    request.predicate = [NSPredicate predicateWithFormat: @"dId == %@", sender.tag];
    request.sortDescriptors = nil;

    // Perform the search
    NSError *error = nil;
    NSArray *matches = [context executeFetchRequest: request error: &error];

    // Use the search results
    if( !matches || matches.count < 1 ) {
        NSLog( @"Uh oh, got a nil back from my Destination fetch request!" );
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"No Info"
                    message: @"The database did not have information for this selection"
                   delegate: nil
          cancelButtonTitle: @"OK"
          otherButtonTitles: nil];
        [alert show];
    } else {
        MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
        movc.destDetails = [matches lastObject];
        [self.navigationController pushViewController: movc animated: YES];
    }
}
@end

Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.

ilmiacs
  • 2,566
  • 15
  • 20
  • Excellent solution... thanks for getting my brain out of a knot. Would the self.databaseReady and self.waitingForDB need to be 'atomic' properties? – LavaSlider Apr 18 '13 at 11:28
  • Good question. But no, here we should be safe without atomic. Setting and reading those properties on the same thread guarantees that the execution can take place in only one of the methods `goButtonTouched` and `handleDatabaseReady` at a time. If we would attempt to call those methods on separate threads, on the other hand, even setting them to atomic would not guarantee correct behavior of my code. Thread safety is tricky... – ilmiacs Apr 18 '13 at 12:50
0

I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.

Vladimir Gritsenko
  • 1,669
  • 11
  • 25
-1

This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:@selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.

sixthcent
  • 1,150
  • 7
  • 6
  • I don't need a "doStuff", everything I need to do is in the sample code given. Also, I don't want the user to ever see a spinner... in most cases it will probably take less than a second to open the database and more than a second for the user to figure out what button they want to press. In which case there are no spinners, no pauses, no delays, and a happy user. But if there is a fast user or a slow database connection I can't have things crashing and I need to indicate to the user that it is working on his or her request not hung-up. – LavaSlider Apr 18 '13 at 11:43
-1

There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.

I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.

In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.

Community
  • 1
  • 1
Mike Weller
  • 45,401
  • 15
  • 131
  • 151