Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Race-free completion and cancellation of asynchronous work.

branch: develop
README.mdown

What

Futures are a way to represent the expected result of some asynchronous action. RXFuture is a class for handling cancellation and completion and the notification of either for asynchronous tasks.

How

Let’s start with a simple asynchronous worker which calls a block with its result when it’s done:

-(void)workWithCompletionHandler:(void(^)(id))block {
    dispatch_async(dispatch_get_global_queue(DISPATCH_PRIORITY_DEFAULT), 0), ^{
        // work work work
        block(@"all done!");
    });
}

So far so good. We’d like to be able to cancel it, though. Let’s start by using a future to handle completion notification:

-(RXFuture *)workWithCompletionHandler:(void(^)(id))block {
    RXFuture *future = [RXFuture new];
    [future onComplete:^{
        block(@"all done!");
    }];
    dispatch_async(dispatch_get_global_queue(DISPATCH_PRIORITY_DEFAULT, 0), ^{
        // work work work
        [future complete];
    });
    return future;
}

Now the caller has something to cancel:

@synthesize future; // assume this exists

-(IBAction)startWorking:(id)sender {
    [self showIndeterminateProgressBar];

    self.future = [worker workWithCompletionHandler:^(id result) {
        // we got a result!
        [self hideIndeterminateProgressBar];
        self.future = nil;
    }];

    [future onCancel:^{
        // clean up anything that might depend on what we attempt to do in the completion handler
        [self hideIndeterminateProgressBar];
        self.future = nil;
    }];
}

-(IBAction)cancel:(id)sender {
    [future cancel];
}

So far, so good—when the user cancels, we clean up the UI as necessary safe in the knowledge that the completion block won’t be called on us. The correct block will be called when the work completes successfully and when the user cancels it early. But the worker doesn’t actually cancel anything; that’s wasteful.

RXFuture supports multiple completion and cancellation handlers; it will call all of the completion handlers when it’s completed, and all of the cancellation handlers when it’s cancelled. You can take advantage of this to have your worker stop when it’s cancelled. Here’s an example that cancels an NSURLConnection when the future is cancelled:

NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
[future onCancel:^{
    [connection cancel];
}];

If you’re implementing an asynchronous worker that needs to pass the result of some calculation to its caller via a block, it’s perfectly valid to call -onComplete: immediately before (or as you’ll see below, after) calling -complete.

id result = …;
[future onComplete:^{ block(result); }]
[future complete];

As a convenience, -complete: and its cancellation analog -cancel: (note the colon!) act as shorthand for this:

id result = …;
[future complete:^{ block(result); }]

If a completion or cancellation block is added after the future is completed or cancelled, it will be dispatched immediately:

[future cancel];
[future onCancel:^{ NSLog(@"this block will be run because the future has already been cancelled."); }];

Work that needs to be done on either cancellation or completion can be handled with -onCleanUp:. It will be called exactly once, when the future enters either of those states:

RXFuture *future = [worker startDoingSomething];
id workerDependentObject = [… new];
[future onCleanUp:^{ [workerDependentObject tidyYourselfUpALittle]; }];

Details

It’s worth being aware of the specific semantics of RXFuture:

  • completion handlers aren’t called when the future is cancelled
  • cancellation handlers aren’t called when the future is completed
  • cleanup handlers (added with -onCleanUp:) are called exactly once when the future is cancelled OR completed
  • multiple calls to -cancel will result in each cancellation handler being called exactly once
  • multiple calls to -complete will result in each completion handler being called exactly once
  • completion handlers added after completion will be dispatched immediately
  • cancellation handlers added after cancellation will be dispatched immediately
  • if your completion handler requires a result that you don’t have when the task is started, it’s okay to call -onComplete: with the appropriate block just before (or after!) calling -complete
  • for the obvious reasons, only workers should call -complete on their futures unless both are designed to handle this; both clients and workers can both call -cancel, however
  • calls to -cancel and -complete are serialized; the first one dequeued wins
  • don’t call -isCompleted and -isCancelled except within a block passed to -performBlock:; in any other context, their return values are potentially obsolete before they’ve returned

Thanks

Thanks to Andy Matuschak for some fascinating thought experiments that kicked this all off, and to Dave Dribin for discussion which refined RXFutures.

Something went wrong with that request. Please try again.