Serializing asynchronous tasks in objective C

China☆狼群 提交于 2019-12-06 05:15:02

问题


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?


回答1:


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.




回答2:


"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.




回答3:


When subclassing NSOperation I would strongly suggest only overriding main unless you really know what you are doing as it is really easy to mess up thread safety. While the documentation says that the operation will not be concurrent the act of running them through an NSOperationQueue automatically makes them concurrent by running them on a separate thread. The non-concurrency note only applies if you call the start method of the NSOperation yourself. You can verify this by noting the thread ID that each NSLog line contains. For example:

2013-09-17 22:49:07.779 AppNameGoesHere[58156:ThreadIDGoesHere] Your log message goes here.

The benefit of overriding main means that you don't have to deal with thread safety when changing the state of the operation NSOperation handles all of that for you. The main thing that is serializing your code is the line that sets maxConcurrentOperationCount to 1. This means each operation in the queue will wait for the next to run (all of them will run on a random thread as determined by the NSOperationQueue). The act of calling dispatch_async inside each operation also triggers yet another thread.

If you are dead set on using subclassing NSOperation then only override main, otherwise I would suggest using NSBlockOperation which seems like what you are somewhat replicating here. Really though I would avoid NSOperation altogether, the API is starting to show its age and is very easy to get wrong. As an alternative I would suggest something like RXPromise or my own attempt at solving this problem, FranticApparatus.



来源:https://stackoverflow.com/questions/18202952/serializing-asynchronous-tasks-in-objective-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!