Thursday, August 30, 2012

Grand Central Dispatch in iOS

What is GCD?
GCD is a low level C API that allows programmers to take advantage of multiple cores in iOS devices, without worrying about which core they execute. GCD achieves a lot of its capabilities using dispatch queue's that are nothing more than thread pools. iOS does not allow you to interact with threads directly. We (programmers) talk to dispatch queue's and they in turn deal with tasks to do the job

Why should you care?
Multiple reasons why you should.
1. Makes the application responsive when doing time consuming tasks
2. Works extremely well with BLOCKs
3. Take advantage of multi-core h/w support

How to do GCD programming
You need no additional frameworks to be added into your project for GCD. All that you need to know is look for dispatch_* APIs and that's it

APIs you will come across here
dispatch_get_main_queue
Returns the serial dispatch queue associated with the application’s main thread.

dispatch_get_global_queue
Returns a well-known global concurrent queue of a given priority level.

dispatch_async
Submits a block for asynchronous execution on a dispatch queue and returns immediately.

dispatch_sync
Submits a block object for execution on a dispatch queue and waits until that block completes.

dispatch_release
Decrements the reference (retain) count of a dispatch object.


Basic GCD

- (IBAction)basicGCDButtonTouched:(id)sender {
    /*
     * Change the que from main queue to the newly created queue you will find out that the code
     * takes a lot more time to execute and that's probably because ios waits on the new queue and then
     * when the main queue becomes available, move the new queue to main!
     * Better programming practice is to do all UI related communication in main queue
     */
    //dispatch_queue_t mainQueue = //dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_async(mainQueue, ^{
        [[[UIAlertView allocinitWithTitle:@"Basic GCD" message:@"A simple GCD message" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:nilnilshow];
    });
}


Now that was easy and you rarely get to see things to do like that in a real life. Let's say if you have 2 background tasks that need to be done and all the while UI should be responsive, here's a simple way to do that. In the case below we execute 2 functions in a queue other than main queue, so the application remains responsive throughout while those long running methods grind their way.


- (IBAction)nonUIGCDButtonTouched:(id)sender {
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT0);
    NSLog(@"THREAD - 1");
    dispatch_sync(concurrentQueue, printFrom1To10000);
    
    NSLog(@"THREAD - 2");
    dispatch_sync(concurrentQueue, printFrom1To10000);
}

If you look at the output of the above code, you will notice that unless THREAD-1 completes, THREAD-2 would not have started. That's because it's a serial execution process. When we load a View/ViewController we talk to a lot of services, we can follow the above methodology to talk to the service. Do remember above call is serial, until the first call is completed, second one will not get invoked.

What if you wanted those methods not to be executed in a serial manner. No sweat here's how we do it


- (IBAction)nonUIGCDAsyncButtonTouched:(id)sender {
    /*
     * This will still block the main thread. How ever the two print operations/methods
     * will execute in parallel. 
     */
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT0);
    NSLog(@"THREAD - 1");
    dispatch_async(concurrentQueue, printFrom1To10000);
    
    NSLog(@"THREAD - 2");
    dispatch_async(concurrentQueue, printFrom1To10000);
}

So far we have been talking about executing in different queue's what about the first promise GCD made - Free my UI for other operations? Here's a simple sample of the above with operable UI too


- (IBAction)gcdSerialButtonTouched:(id)sender {
    /*
     * Queue name should be unique across applications. Make sure you pick a name that will not conflict
     * with anything else. It will not be a bad idea to have a seperate class to generate unique names
     * for these queue's
     */
    dispatch_queue_t serial_queue = dispatch_queue_create("CHASE.serial.queue"0);
    /*
     * Although these methods are async, they will be processed serially
     * This is a wonderful way to execute things in an auxillary thread, serially
     */
    dispatch_async(serial_queue, ^{
        [self firstOperationEntryPoint:@"First operation"];
    });
    dispatch_async(serial_queue, ^{
        [self secondOperationEntryPoint:@"Second operation"];
    });
    dispatch_release(serial_queue);
    
}

In the above code I have invoked 2 other functions - firstOperationEntryPoint & secondOperationEntryPoint. Here 's the code for those


- (void)firstOperationEntryPoint:(id)paramObject {
    for(NSUInteger counter =0; counter<1000;counter++) {
        NSLog(@"Counter: %lu - Thread = %@", (unsigned long)counter, [NSThread currentThread]);
    }
    NSLog(@"Parameter: %@", paramObject);
    NSLog(@"Main thread: %@", [NSThread mainThread]);
    NSLog(@"Current thread: %@", [NSThread currentThread]);
}

- (void)secondOperationEntryPoint:(id)paramObject {
    for(NSUInteger counter =0; counter<1000;counter++) {
        NSLog(@"Counter: %lu - Thread = %@", (unsigned long)counter, [NSThread currentThread]);
    }
    NSLog(@"Parameter: %@", paramObject);
    NSLog(@"Main thread: %@", [NSThread mainThread]);
    NSLog(@"Current thread: %@", [NSThread currentThread]);
}


Above code executes in serial fashion. If we want them to be in non-serial mode then group the calls and execute them using dispatch_group_async API

Now that's a nice way to get started with groups. Let's say you have a bunch of call that need to be executed and when all of them are complete you need to be notified. Here's how we can do that


- (IBAction)simpleGCDGroupingButtonTouched:(id)sender {
    dispatch_group_t taskGroup = dispatch_group_create();
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_group_async(taskGroup, customQueue, printFrom1To10000);
    dispatch_group_async(taskGroup, customQueue, printFrom1To10000);
    
    dispatch_group_notify(taskGroup, mainQueue, ^{
        [[[UIAlertView allocinitWithTitle:@"GCD Async Grouping" message:@"Async jobs in this group are complete" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:nilnilshow];
    });
}


This is typically useful when you want to fire a lot of download or service calls. Instead of constantly asking for completion (poll), dispatch_group_notify is a better choice.

So far we had a look at low leve C API, if we want to handle it with higher level functionality iOS has fed us with NSoperation's


 (IBAction)asyncOperationsButtonTouched:(id)sender {
    NSOperationQueue *operationQueue = [[[NSOperationQueue allocinitautorelease];
    NSInvocationOperation *firstOperation = [[NSInvocationOperation allocinitWithTarget:self selector:@selector(firstOperationEntryPoint:) object:@"first operation"];
    NSInvocationOperation *secondOperation = [[NSInvocationOperation allocinitWithTarget:self selector:@selector(secondOperationEntryPoint:) object:@"second operation"];
    // if not for dependency, they will execute parallely
    [secondOperation addDependency:firstOperation];
    [operationQueue addOperation:secondOperation];
    [operationQueue addOperation:firstOperation];   
}


Here's a more complete example. If we want to download a file from a remote server and display it in view, we all know the time it will take will vary depending on the bandwidth available for download. We cannot keep the user waiting until the download is complete. If it takes too much time the user should be able to move out of the screen and can comeback later for download. How can we do that with dispatch_* APIs


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
                                                                    0);
    [_activationIndicator setCenter:self.view.center];
    [_activationIndicator setHidesWhenStopped:YES];
    [self.view addSubview:_activationIndicator];
    [_activationIndicator startAnimating];
    dispatch_async(concurrentQueue, ^{
        __block UIImage* image = nil;
            dispatch_sync(concurrentQueue, ^{
            /* Download code */
            NSString *urlString = @"http://images.apple.com/mobileme/features/images/ipad_findyouripad_20100518.jpg";
            NSURL *url = [[NSURL alloc] initWithString:urlString];
            NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
            NSError *downloadError = nil;
            NSData *imageData = [NSURLConnection sendSynchronousRequest:urlRequest
                                                      returningResponse:nil
                                                                  error:&downloadError];
            if(downloadError == nil && imageData != nil) {
                image = [UIImage imageWithData:imageData];
            } else if(downloadError != nil) {
                NSLog(@"ERROR: %@", downloadError);
            } else {
                NSLog(@"No data could be downloaded from the URL!");
            }
                [NSThread sleepForTimeInterval:5];
        });
        
        dispatch_sync(dispatch_get_main_queue(), ^{
            /* Display it in UIImage */
            if(image != nil) {
                [_imageView setImage:image];
                [_imageView setContentMode:UIViewContentModeScaleAspectFit];
                [self.view addSubview:_imageView];
                [_activationIndicator stopAnimating];
                [_activationIndicator removeFromSuperview];
                NSLog(@"Image downloaded!");
            } else {
                NSLog(@"Image isn't downloaded for display!");
            }
        });
    });
}

- (void)displayAlert {
    dispatch_queue_t concurrentQue = dispatch_get_main_queue();
    dispatch_async(concurrentQue, ^{
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Sample Alert" message:@"From Sync block" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:nil, nil];
        [alertView show];
        [alertView release];
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        [_activationIndicator startAnimating];
    });
}


Issues with GCD
To be updated shortly

No comments: