Serializing Asynchronous Tasks in a C Object

I wanted to be able to serialize "truly" asynchronous methods, for example:

  • web request creation
  • shows UIAlertView

This is usually a complex business, and most samples of sequential queues show "sleep" in the NSBlockOperation block. This does not work because the operation is completed only when the callback occurs.

I had to implement this by subclassing NSOperation, here are the most interesting implementation bits:

+ (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 demo ...

 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 sleep, but made sure they were running in the background thread to simulate a network call. The magazine reads as follows

 1... 1 2... 2 

Which is optional. What is wrong with this approach? Are there any reservations I should be aware of?

+4
source share
3 answers

At first glance, this will work; some parts are missing in order to have the “right” subclass of NSOperation, though.

You cannot handle the "canceled" state, you should check isCancelled at the beginning and not start if this returns YES ( "response to cancel command" )

And the isConcurrent method isConcurrent also be overridden, but you may have omitted this for brevity.

+1
source

The "serialization" of asynchronous tasks will actually be called the "continuation" (see also this wiki Continuation article.

Suppose your tasks can be defined as an asynchronous function / method with a completion handler whose parameter is the end result of the asynchronous task, for example:

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

Having blocks , the "continuation" can be easily completed by calling the following asynchronous task from the previous task completion block:

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

Note that the foo method gets "infected" asynchronously ;;)

That is, here the possible effect of the foo method, namely, printing the user's response to the console, is actually again asynchronous.

However, the "chain" of several asynchronous tasks, that is, the "continuation" of several asynchronous tasks, can become quickly cumbersome:

Implementing a “continue” with completion blocks will increase the indentation for each task completion handler. In addition, by implementing a tool that allows the user to cancel tasks in any state, as well as implement code to handle error conditions, the code becomes quite confusing, difficult to write, and difficult to understand.

The best approach to implementing continuation, as well as canceling and processing errors, uses the concept of Futures or Promises . A future or promise is the end result of an asynchronous task. Basically, this is just a different approach to the “signal of the end result” to the call site.

In Objective-C, "Promise" can be implemented as a regular class. There are third-party libraries that implement the "promise." The following code uses a specific implementation, RXPromise.

When using such a promise, you define your tasks as follows:

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

Note. The completion handler does not exist.

With the promise, the result of the asynchronous task will be obtained using the success or error handler, which will be registered using the then property of the promise. Either success or an error handler is called by the task at its completion: when it succeeds, the success handler will be called by passing its result to the result of the success handler parameter. Otherwise, when the task fails, it passes the reason to the error handler - usually an NSError object.

The main use of 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 the result of a parameter of type id . The error handler block has an argument of type NSError .

Note that the promise.then(...) operator returns a promise that represents the result of any handler that is called when the “parent” promise was resolved either with success or with an error. The return value of the handler can be either an "immediate result" (by some object) or an "end result" represented as a Promise object.

A commented example of an OP problem is shown in the following code snippet (including complex 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 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, of course, requires more explanation. For a detailed and more detailed description and how cancellation can be achieved at any given time, you can look at the RXPromise library - an Objective-C class that implements the "Promise". Disclosure: I am the author of the RXPromise library.

+7
source

When subclassing NSOperation, I would strongly recommend only overriding main if you really don't know what you are doing, because it is very easy to damage thread safety. Although the documentation says that the operation will not be simultaneous, the action of running them through NSOperationQueue automatically makes them parallel, running them in a separate thread. The non concurrency remark applies only if you yourself call the NSOperation start method. You can verify this by checking the stream identifier that each NSLog line contains. For instance:

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

The advantage of overriding fixed assets means that you do not have to deal with thread safety when changing the status of an operation. NSOperation handles all this for you. The main thing that serializes your code is the line that sets maxConcurrentOperationCount to 1. This means that every operation in the queue will wait for the next run (they will all be executed on a random thread, as defined by NSOperationQueue). The action of calling dispatch_async inside each operation also calls another thread.

If you are configured to use the NSOperation subclass, then just override main, otherwise I would suggest using NSBlockOperation, which seems to be what you are replicating to some extent here. In fact, although I would have avoided NSOperation altogether, the API is starting to show its age and it is very easy to make mistakes. As an alternative, I would suggest something like RXPromise or my own attempt to solve this problem, FranticApparatus .

0
source

Source: https://habr.com/ru/post/1496729/


All Articles