Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serializing asynchronous tasks in objective C

I wanted to be able to serialize 'genuinely' async methods, for example:

  • making a web request
  • showing a UIAlertView

This is typically a tricky business and most samples of serial queues show a 'sleep' in an NSBlockOperation's block. This doesn't work, because the operation is only complete when the callback happens.

I've had a go at implementing this by subclassing NSOperation, here's the most interesting bits of the implementation:

+ (MYOperation *)operationWithBlock:(CompleteBlock)block
{
    MYOperation *operation = [[MYOperation alloc] init];
    operation.block = block;
    return operation;
}

- (void)start
{
    [self willChangeValueForKey:@"isExecuting"];
    self.executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
    if (self.block) {
        self.block(self);
    }
}

- (void)finish
{
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];
    self.executing = NO;
    self.finished = YES;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished
{
    return self.finished;
}

- (BOOL) isExecuting
{
    return self.executing;
}

This works well, here's a demonstration...

NSOperationQueue *q = [[NSOperationQueue alloc] init];
q.maxConcurrentOperationCount = 1;

dispatch_queue_t queue = dispatch_queue_create("1", NULL);
dispatch_queue_t queue2 = dispatch_queue_create("2", NULL);

MYOperation *op = [MYOperation operationWithBlock:^(MYOperation *o) {
    NSLog(@"1...");
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1");
        [o finish]; // this signals we're done
    });
}];

MYOperation *op2 = [MYOperation operationWithBlock:^(MYOperation *o) {
    NSLog(@"2...");
    dispatch_async(queue2, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2");
        [o finish]; // this signals we're done
    });
}];

[q addOperations:@[op, op2] waitUntilFinished:YES];

[NSThread sleepForTimeInterval:5];

Note, I also used a sleep but made sure these were executing in background thread to simulate a network call. The log reads as follows

1...
1
2...
2

Which is as desired. What is wrong with this approach? Are there any caveats I should be aware of?

like image 536
ConfusedNoob Avatar asked Feb 15 '23 14:02

ConfusedNoob


2 Answers

"Serializing" asynchronous tasks will be named actually "continuation" (see also this wiki article Continuation.

Suppose, your tasks can be defined as an asynchronous function/method with a completion handler whose parameter is the eventual result of the asynchronous task, e.g.:

typedef void(^completion_handler_t)(id result);

-(void) webRequestWithCompletion:(completion_handler_t)completionHandler;
-(void) showAlertViewWithResult:(id)result completion:(completion_handler_t)completionHandler;

Having blocks available, a "continuation" can be easily accomplished through invoking the next asynchronous task from within the previous task's completion block:

- (void) foo 
{
    [self webRequestWithCompletion:^(id result) {  
        [self showAlertViewWithResult:result completion:^(id userAnswer) {
            NSLog(@"User answered with: %@", userAnswer);
        }
    }
}

Note that method foo gets "infected by "asynchrony" ;)

That is, here the eventual effect of the method foo, namely printing the user's answer to the console, is in fact again asynchronous.

However, "chaining" multiple asynchronous tasks, that is, "continuing" multiple asynchronous tasks, may become quickly unwieldy:

Implementing "continuation" with completion blocks will increment the indentation for each task's completion handler. Furthermore, implementing a means to let the user cancel the tasks at any state, and also implement code to handle the error conditions, the code gets quickly confusing, difficult to write and difficult to understand.

A better approach to implement "continuation", as well as cancellation and error handling, is using a concept of Futures or Promises. A Future or Promise represents the eventual result of the asynchronous task. Basically, this is just a different approach to "signal the eventual result" to the call site.

In Objective-C a "Promise" can be implemented as an ordinary class. There are third party libraries which implement a "Promise". The following code is using a particular implementation, RXPromise.

When utilizing such a Promise, you would define your tasks as follows:

-(Promise*) webRequestWithCompletion;
-(Promise*) showAlertViewWithResult:(id)result;

Note: there is no completion handler.

With a Promise, the "result" of the asynchronous task will be obtained via a "success" or an "error" handler which will be "registered" with a then property of the promise. Either the success or the error handler gets called by the task when it completes: when it finishes successfully, the success handler will be called passing its result to the parameter result of the success handler. Otherwise, when the task fails, it passes the reason to the error handler - usually an NSError object.

The basic usage of a Promise is as follows:

Promise* promise = [self asyncTasks];
// register handler blocks with "then":
Promise* handlerPromise = promise.then( <success handler block>, <error handler block> );

The success handler block has a parameter result of type id. The error handler block has a parameter of type NSError.

Note that the statement promise.then(...) returns itself a promise which represents the result of either handler, which get called when the "parent" promise has been resolved with either success or error. A handler's return value may be either an "immediate result" (some object) or an "eventual result" - represented as a Promise object.

A commented sample of the OP's problem is shown in the following code snippet (including sophisticated error handling):

- (void) foo 
{
    [self webRequestWithCompletion] // returns a "Promise" object which has a property "then"
    // when the task finished, then:
    .then(^id(id result) {
        // on succeess:
        // param "result" is the result of method "webRequestWithCompletion"
        return [self showAlertViewWithResult:result];  // note: returns a promise
    }, nil /*error handler not defined, fall through to the next defined error handler */ )       
    // when either of the previous handler finished, then:
    .then(^id(id userAnswer) {
        NSLog(@"User answered with: %@", userAnswer);
        return nil;  // handler's result not used, thus nil.
    }, nil)
    // when either of the previous handler finished, then:
    .then(nil /*success handler not defined*/, 
    ^id(NEError* error) {
         // on error
         // Error handler. Last error handler catches all errors.
         // That is, either a web request error or perhaps the user cancelled (which results in rejecting the promise with a "User Cancelled" error)
         return nil;  // result of this error handler not used anywhere.
    });

}

The code certainly requires more explanation. For a detailed and a more comprehensive description, and how one can accomplish cancellation at any point in time, you may take a look at the RXPromise library - an Objective-C class which implements a "Promise". Disclosure: I'm the author of RXPromise library.

like image 75
CouchDeveloper Avatar answered Feb 27 '23 06:02

CouchDeveloper


At a first glance this would work, some parts are missing to have a "proper" NSOperation subclass though.

You do not cope with the 'cancelled' state, you should check isCancelled in start, and not start if this returns YES ("responding to the cancel command")

And the isConcurrent method needs to be overridden too, but maybe you omitted that for brevity.

like image 23
Pieter Avatar answered Feb 27 '23 06:02

Pieter