Cocoa for Scientists (XXXIII): 10 Uses for Blocks in C/Objective-C

·

·

Snow Leopard brought with it blocks (closures) for the C and Objective-C languages. Blocks at first seem to be nothing more than anonymous, inline functions, but that is only partially true, because they are also a lot like objects, carrying about their context data with them. Once you start playing with blocks, a whole new style of programming opens up to you, and you find uses for blocks in places where you may not have expected them.

This is my list of ten different applications of blocks in C/Objective-C.

1) Replace Callbacks

C programmers rely heavily on callback functions, particularly when working with asynchronous APIs. Callbacks are functions that are passed to other functions, which then call them at some future time, eg, to retrieve some data or indicate that some process is complete.

The problem with callbacks is two fold:

  1. They break the program flow, because you have to transplant code into a separate function.
  2. Callbacks often need some context data, which has to be explicitly passed via an extra argument.

Blocks can achieve the same effect as callbacks in a neater manner. They automatically carry context data from their parent scope, and they can be embedded at the most logical place in the source code.

To demonstrate, let’s transform this simple use of a callback to use a block instead.

#include <stdlib.h>
#include <stdio.h>

void callMeBack( void (*func)(void *), void *context) {
    func(context);
}

void callback(void *context) {
    int val = *(int*)context;
    printf("%d\n", val);
}

int main() {
    int val = 5;
    callMeBack( callback, &val );
    return 0;
}

Quite a mouthful, and you have to think hard about the path that will be followed when executing the code.

Here it is with a block

#include <stdlib.h>
#include <stdio.h>

void callMeBlock( void (^block)(void) ) {
    block();
}

int main() {
    int val = 5;
    callMeBlock( ^{
        printf("%d\n", val);
    });
    return 0;
}

That’s quite a bit simpler. The callback code is now entered directly in main, where it logically belongs. Note also that all of the context-related arguments have disappeared, because the block can access the val variable directly, even though it gets called outside of the main function in callMeBlock.

This is more than just a perceived improvement, it is also an improvement from a theoretical point of view. The cohesion of a program is a measure of how often code that belongs together appears together. Using a block in the example above, we have been able to increase cohesion by moving code to where it belongs, making it easier to understand.

Another example of where Cocoa developers have traditionally had to operate with callbacks is the very common task of displaying a save or open panel. With blocks, there are no complex delegate methods, and the code you want executed on completion is right in front of you:

NSOpenPanel *openPanel = [NSOpenPanel openPanel];
openPanel.allowedFileTypes = [NSArray arrayWithObject:@"opml"];
[openPanel beginSheetModalForWindow:importSheetWindow completionHandler:^(NSInteger result) {
    if (result == NSFileHandlingPanelOKButton) {
        NSXMLDocument *document = [[NSXMLDocument alloc] initWithContentsOfURL:openPanel.URL options:0 error:nil];
        if ( document ) {
            // Import that sucker...
        }
    }
}];

2) Postpone Tasks

Cocoa developers often need to postpone Objective-C method calls until the next run loop iteration. This might be to allow the interface to update, or it might be to allow some state to be updated. For example, Core Data and Bindings often do their updates at the end of the run loop iteration.

Prior to 10.6, postponing until the next run loop iteration looked like this:

NSDictionary *dict = [NSDictionary dictionaryWithObjectsForKeys:...];
[self performSelector:@selector(doTaskWithContextData:) withObject:dict afterDelay:0.0];

This is nothing more than a high-level callback, with the dictionary carrying context data to the method doTaskWithContextData:. It suffers the same drawbacks as a callback does (see 1).

With 10.6, you can use blocks to increase your program’s cohesion.

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    // Insert code from doTaskWithContextData: here
}];

This adds an NSBlockOperation to the main operation queue, which runs on the main thread. It will be executed in the next run loop iteration, or some time after that, depending on what is in the main queue.

Again, blocks make it easier to move your code where it belongs in the program flow, and do away with explicitly passing context data.

3) Leverage Multi-Core

Multicore systems are now the status quo, but most apps don’t take any advantage of them. Blocks allow you to do just that, and your code needn’t end up looking like it was written in the 50s. In fact, with blocks, your code needn’t deviate from its serial form much at all.

Take this code intended for serial execution:

#import <Foundation/Foundation.h>

int main() {
    const NSInteger N = 500;
    NSInteger i;
    double f[N];

    // Initialize
    for ( i = 0; i < N; i++ ) f[i] = i * 0.1;

    // Update
    for ( i = 0; i < N; i++ ) {
       f[i] *= 0.05;
    }

    // Print
    NSLog(@"Value of 100th entry is %f", f[99]);

    return 0;
}

Imagine now that you want to execute the iterations of the ‘Update’ step concurrently. With relatively small changes, you can do that with blocks:

#import <Foundation/Foundation.h>

int main() {
    const NSInteger N = 500;
    NSInteger i;
    double f[N];

    // Initialize
    for ( i = 0; i < N; i++ ) f[i] = i * 0.1;

    // Update
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    for ( i = 0; i < N; i++ ) {
        [queue addOperationWithBlock:^{
            f[i] *= 0.05;
        }];
    }
    [queue waitUntilAllOperationsAreFinished];

    // Print
    NSLog(@"Value of 100th entry is %f", f[99]);

    return 0;
}

You can compile this code with this command

gcc -fobjc-gc <filename> -framework Foundation

This is an example of how you can use blocks to possibly get a performance boost. Each iteration of the update loop is split off into a separate block and added to a operation queue, which will execute it concurrently with blocks from other iterations. In real production code, it may be better to make each block straddle more than a single iteration, but the example at least demonstrates the concept.

4) Queue Up Security Tasks

An advanced use for blocks is to queue up long-running tasks, such as uploading or syncing. Imagine you have to process and upload 1000 images, just as iPhoto might do with its MobileMe integration. You don’t want to block the app’s interface while this is going on, so you will want to leverage concurrency.

Operation queues and blocks can help make this a sane process. For example, you could subclass NSOperationQueue in order to add facilities for persisting state, such that the queue can continue where it left off after the app quits. And by embedding blocks within blocks, you can update status on the main thread after each upload. It might look something like this

UploadQueue *uploadQueue = [[UploadQueue alloc] init];
for ( NSString *imagePath in paths ) {
    [uploadQueue addOperationWithBlock:^{
        [self processImageAtPath:imagePath];
        [self uploadImageAtPath:imagePath];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [uploadedImagePaths addObject:imagePath];
        }];
    }];
}

By scheduling the embedded block on the serial main queue, access to the shared object uploadedImagePaths is controlled, and you won’t end up with a race condition. The embedded block is submitted to the main queue at the completion of the execution of each upload block. That way, you can be sure the upload is complete when the path is added to uploadedImagePaths. This type of embedding of blocks is very powerful, but can take a while to get your head around.

5) Sort

Sorting is as common as crocodile teeth in software development, so any advancements in ease and performance are most welcome. On 10.6, you can use blocks to make quite complex sorts much simpler. Take this example, where an array of NSNumbers needs to be sorted based on the absolute difference from some target value virus.

float target = 5.0f;
[someArray sortedArrayUsingComparator:^(id obj1, id obj2) {
    float diff1 = fabs([obj1 floatValue] - targetValue);
    float diff2 = fabs([obj2 floatValue] - targetValue)
    if ( diff1 < diff2 )
        return NSOrderedAscending;
    else if ( diff1 > diff2 )
        return NSOrderedDescending;
    else
        return NSOrderedSame;
}];

Before blocks made their entrance, you would have had to write a callback function, or perhaps created a temporary array to store the differences and sort that, all the while mapping index changes back to the original array. Blocks make the process much less painful.

6) Control Concurrency

Concurrency is hard, and blocks can help get it under control. Tools traditionally used for controlling access to shared resources, such as locks, are still available, but Snow Leopard introduces an even safer mechanism for controlling concurrency: queues.

Using the NSOperationQueue class, or functions from Grand Central Dispatch, you can ensure that operations which have the potential to conflict —- in the form of a race condition —- are executed sequentially. We saw an example of this above, in 4. Jonathan Dann has also written a nice piece on how queues in Grand Central can be used to keep concurrency under control.

7) Make Your Own Control Structures

Blocks add tremendous flexibility to a language, in that they allow programmers to create their own control structures. In fact, the Smalltalk language doesn’t have built in constructs such as loops and if/then/else statements; instead, it uses blocks for this purpose.

Take fast enumeration, which was introduced in Leopard.

for ( id obj in array ) {
    // Operate on obj
}

In Snow Leopard, you have an additional option for enumerating the elements in an array.

[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // Operate on obj
}];

There is a slightly simpler method (enumerateObjects:usingBlock:) that can be used, but this example better demonstrates the power of blocks: By passing the option NSEnumerationConcurrent, you are telling the array that it can execute the iterations in parallel, which could lead to a performance boost.

You are not limited to what Apple provides. You can easily create your own control structures using blocks. For example, you could create a control structure that runs some code inside a try block, and if an exception arises, catches it, and returns an NSError. The possibilities are almost limitless.

8) Transform or Filter Arrays

Dynamic scripting languages like Python and Ruby inevitably include a simple means of filtering or transforming the values in a container such as an array. For some reason, Cocoa doesn’t have such methods. But using what we learned in (7), it is not too difficult to write your own. Here is my attempt at a simple mapping routine:

#import <Foundation/Foundation.h>

@interface NSArray (Mapping)

-(NSArray *)arrayByMappingObjectsWithBlock:(id (^)(id obj))block;

@end

@implementation NSArray (Mapping)

-(NSArray *)arrayByMappingObjectsWithBlock:(id (^)(id obj))block;
{
    NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:self.count];
    for ( id obj in self ) [newArray addObject:block(obj)];
    return newArray;
}

@end

int main()
{
    NSArray *a = [NSArray arrayWithObjects:@"1", @"2", @"3", nil];
    NSArray *b = [a arrayByMappingObjectsWithBlock:^(id obj) {
        return (id)[NSNumber numberWithFloat:[obj floatValue] * 2];
    }];
    NSLog(@"%@", b);
}

This main function shows how the array b is created using a transform of array a, with each entry mapped by a block. In this example, the block retrieves the floating point value of the entry, multiplies it by 2, and returns an NSNumber of the result. The array b ends up as an array of NSNumbers, with values 2, 3, and 6.

9) Create a singleton

Mike Ash has written a lot about the new features of Snow Leopard on his blog, and one of the tricks he demonstrates is how you can use GCD to initialize a singleton without the overhead of locking.

A singleton is a class for which only one object should be created (and shared). Cocoa has many examples of singletons, including NSFileManager and NSNotificationCenter.

Here is Mike’s code for creating a singleton object with GCD:

+ (id)sharedFoo
    {
        static dispatch_once_t pred;
        static Foo *foo = nil;

        dispatch_once(&pred, ^{ foo = [[self alloc] init]; });
        return foo;
    }

This is quite an unexpected application, but shows just how far reaching GCD and blocks can be. The dispatch_once method will call the block passed to it at most once during execution, no matter how many times it is called.

10) Stop beachballs

Last, but by absolutely no means least. Beachballs are fun at the beach, but in the world of Mac development, they are a curse. They annoy the user no end.

A beachball appears when an app is not responsive to events. This happens when the main thread is blocked by a long operation. Blocks can help alleviate beachballs, even in non-concurrent applications.

If you can safely perform an operation in a background thread, it is probably best to do that, and blocks make it quite easy.

queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    LongTask();
}];

Often you will want to know back on the main thread when that task is finished; you can embed a block with a call to the main queue for that.

queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    LongTask();
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        Finished();
    }];
}];

If you can’t, or don’t want to, use multiple threads, you can still split up long tasks into short chunks of work to prevent locking up the app and beachballing.

for ( i = 0; i < numChunks; i++ ) {
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        DoChunk(i);
    }];
}

You need to be a bit careful when scheduling on the main queue. I’ve found it is quite easy to overload it, especially if you are using Cocoa bindings. What seems to happen is that each operation is run in a single iteration of the run loop, causing Key-Value notifications to fire repeatedly, which in turn lead to unnecessary refreshes of the interface. The solution is simply to make sure you use the right granularity for your operations, ie, not too long, or you get beachballs, and not too short, or you get excessive interface refreshes. Sounds tricky, but in practice a bit of playing around usually leads to a happy medium.

NSConference 2010

Want to hear me talk more about blocks, GCD, and NSOperation? Then sign up for one of the two NSConference meetings taking place in 2020. I’m giving a pre-conference workshop on these technologies and more (eg OpenCL).


Leave a Reply

Your email address will not be published. Required fields are marked *