2

I want to perform few operations and need to start the next operation only upon completion of the previous one. The operation I'm adding will send async call to the server and receive data. I want to start the next operation only after the first call to the server finish receiving data from the server. How to do that?

{.... 
     PhotoDownloader *pd = [[PhotoDownloader alloc] init];
     [GetGlobalOperationQueue addOperation:pd]; 
}

Inside the PhotoDownloader I will allocate the required parameters and call a Global function which handles all the request

[GlobalCommunicationUtil sendServerReq:reqObj withResponseHandler:self];

Inside the sendServerReq method I will construct the URL request and send it to the server and this call is a "sendAsynchronousRequest" call. The PhotoDownloader will have the CommunicationUtil's delegate methods.

Arock
  • 397
  • 5
  • 17

3 Answers3

20

There are two parts to this question:

  1. You asked:

    How do I make one operation not start until the previous operation finishes?

    To do this, you could, theoretically, simply make a serial queue (which is fine if you want to make all operations wait until the prior one finishes). With an NSOperationQueue, you achieve that simply by setting maxConcurrentOperationCount to 1.

    Or, a little more flexible, you could establish dependencies between operations where dependencies are needed, but otherwise enjoy concurrency. For example, if you wanted to make two network requests dependent upon the completion of a third, you could do something like:

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 4;   // generally with network requests, you don't want to exceed 4 or 5 concurrent operations;
                                             // it doesn't matter too much here, since there are only 3 operations, but don't
                                             // try to run more than 4 or 5 network requests at the same time
    
    NSOperation *operation1 = [[NetworkOperation alloc] initWithRequest:request1 completionHandler:^(NSData *data, NSError *error) {
        [self doSomethingWithData:data fromRequest:request1 error:error];
    }];
    
    NSOperation *operation2 = [[NetworkOperation alloc] initWithRequest:request2 completionHandler:^(NSData *data, NSError *error) {
        [self doSomethingWithData:data fromRequest:request2 error:error];
    }];
    
    NSOperation *operation3 = [[NetworkOperation alloc] initWithRequest:request3 completionHandler:^(NSData *data, NSError *error) {
        [self doSomethingWithData:data fromRequest:request3 error:error];
    }];
    
    [operation2 addDependency:operation1];   // don't start operation2 or 3 until operation1 is done
    [operation3 addDependency:operation1];
    
    [queue addOperation:operation1];         // now add all three to the queue
    [queue addOperation:operation2];
    [queue addOperation:operation3];
    
  2. You asked:

    How do I ensure that an operation will not complete until the asynchronous network request it issued has finished as well?

    Again, there are different approaches here. Sometimes you can avail yourself with semaphores to make asynchronous process synchronous. But, much better is to use a concurrent NSOperation subclass.

    An "asynchronous" NSOperation is simply one that will not complete until it issues a isFinished notification (thereby allowing any asynchronous tasks it initiates to finish). And an NSOperation class specifies itself as an asynchronous operation simply by returning YES in its isAsynchronous implementation. Thus, an abstract class implementation of an asynchronous operation might look like:

    //  AsynchronousOperation.h
    
    @import Foundation;
    
    @interface AsynchronousOperation : NSOperation
    
    /**
     Complete the asynchronous operation.
    
     If you create an asynchronous operation, you _must_ call this for all paths of execution
     or else the operation will not terminate (and dependent operations and/or available 
     concurrent threads for the operation queue (`maxConcurrentOperationCount`) will be blocked.
     */
    - (void)completeOperation;
    
    @end
    

    and

    //
    //  AsynchronousOperation.m
    //
    
    #import "AsynchronousOperation.h"
    
    @interface AsynchronousOperation ()
    
    @property (getter = isFinished, readwrite)  BOOL finished;
    @property (getter = isExecuting, readwrite) BOOL executing;
    
    @end
    
    @implementation AsynchronousOperation
    
    @synthesize finished  = _finished;
    @synthesize executing = _executing;
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            _finished  = NO;
            _executing = NO;
        }
        return self;
    }
    
    - (void)start {
        if (self.isCancelled) {
            if (!self.isFinished) self.finished = YES;
            return;
        }
    
        self.executing = YES;
    
        [self main];
    }
    
    - (void)completeOperation {
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished  = YES;
    }
    
    #pragma mark - NSOperation methods
    
    - (BOOL)isAsynchronous {
        return YES;
    }
    
    - (BOOL)isExecuting {
        @synchronized(self) { return _executing; }
    }
    
    - (BOOL)isFinished {
        @synchronized(self) { return _finished; }
    }
    
    - (void)setExecuting:(BOOL)executing {
        [self willChangeValueForKey:@"isExecuting"];
        @synchronized(self) { _executing = executing; }
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    - (void)setFinished:(BOOL)finished {
        [self willChangeValueForKey:@"isFinished"];
        @synchronized(self) { _finished = finished; }
        [self didChangeValueForKey:@"isFinished"];
    }
    
    @end
    

    Now that we have that abstract, asynchronous NSOperation subclass, we can use it in our concrete NetworkOperation class:

    #import "AsynchronousOperation.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    typedef void(^NetworkOperationCompletionBlock)(NSData * _Nullable data, NSError * _Nullable error);
    
    @interface NetworkOperation : AsynchronousOperation
    
    @property (nullable, nonatomic, copy) NetworkOperationCompletionBlock networkOperationCompletionBlock;
    @property (nonatomic, copy) NSURLRequest *request;
    
    - (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    and

    //  NetworkOperation.m
    
    #import "NetworkOperation.h"
    
    @interface NetworkOperation ()
    
    @property (nonatomic, weak) NSURLSessionTask *task;
    
    @end
    
    
    @implementation NetworkOperation
    
    - (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler {
        self = [self init];
    
        if (self) {
            self.request = request;
            self.networkOperationCompletionBlock = completionHandler;
        }
    
        return self;
    }
    
    - (void)main {
        NSURLSession *session = [NSURLSession sharedSession];
    
        NSURLSessionTask *task = [session dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
            if (self.networkOperationCompletionBlock) {
                self.networkOperationCompletionBlock(data, error);
                self.networkOperationCompletionBlock = nil;
            }
    
            [self completeOperation];
        }];
    
        [task resume];
    
        self.task = task;
    }
    
    - (void)cancel {
        [super cancel];
    
        [self.task cancel];
    }
    
    @end
    

    Now, in this example, I'm using block-based implementation of these asynchronous network requests, but the idea works equally well in delegate-based connections/sessions, too. (The only hassle is that NSURLSession specifies its task-related delegate methods to be part of the session, not the network task.)

    Clearly the implementation of your own NetworkOperation class may differ wildly (use delegate patterns or completion block patterns, etc.), but hopefully this illustrates the idea of a concurrent operation. For more information, see the Operation Queues chapter of the Concurrency Programming Guide, notably the section titled "Configuring Operations for Concurrent Execution".

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Hi, Your 2nd explanation answered my question. Yes, I'm using "sendAsynchronousRequest" and I've a doubt on how to handle if I'm calling a global/util method from the operation with the params and that global/util method is sending the async request and it already has a completion block. I've done it the other way. I'm assigning the operation queue to the operation itself as a property and the very first line will suspend the Operation Queue. I'll resume the Operation Queue once the async request finishes and calls the delegate in the operation. It works as I wish, is there any drawback? – Arock Jul 25 '14 at 05:51
  • You definitely don't want to be suspending the queue or anything like that (because you may (eventually) have several requests running concurrently). I might repeat the completion block pattern (see first part of http://stackoverflow.com/a/21736595/1271826) in this global util method and then supply `completeOperation` In the completion handler you provide to this global util method. Or you could just use `sendSynchronousRequest` instead, and the whole problem goes away (but make sure you never call that method from main queue). – Rob Jul 25 '14 at 11:17
  • If you need additional help, please update your question with relevant code samples. – Rob Jul 25 '14 at 11:20
  • I've included the code snippet. Can you guide me now? – Arock Jul 30 '14 at 04:29
  • @Arock Inside `sendServerReq`, you say you're using `sendAsynchronousRequest`. It's not clear from your code snippet, though, how `sendServerReq` informs the caller that the download is complete. You have a `withResponseHandler`, though: Do I infer that you have some protocol by which `sendServerReq` will inform the `responseHandler` that the download is done? So, you'd have `main` (in my code snippet) call `sendServerReq`, but in the delegate method for completion of the request would then call `completeOperation`. – Rob Jul 30 '14 at 05:37
  • Yes, you are correct. sendServerReq will send req using `sendAsynchronousRequest` and it will call a delegate upon receiving the data from the server. – Arock Jul 30 '14 at 08:48
  • +1 for this answer, but when I call cancelAllOperations in viewWillDisappear, in the queue.operations there are no operations... the count is 0. Why? I'm adding them properly and after adding I print the number and it is ok, but in other methods the count is 0. OperationQueue is public variable. – Slavcho Dec 16 '16 at 14:22
  • @Slavcho - If you know that there are network tasks that are still underway (confirm this), if you don't see anything in your queue that is either because (a) the asynchronous operation base class wasn't implemented correctly or, more likely, (b) you're not referring to the same queue (check to make sure the queue is not `nil` and that it's the same queue as shown above by looking at the address of the queue object). It's going to be something simple like that. – Rob Dec 16 '16 at 20:12
  • Unrelated, note that this was using fairly dated `NSOperation` subclass properties and nowadays we use `isAsynchronous`, not `isConcurrent`. Also, I noticed that this wasn't synchronizing the properties. None of those changes affect what you're doing, Slavcho, but I mention it because I'm updated my above answer to reflect best practice re operations. – Rob Dec 16 '16 at 20:13
  • Hmmm, setting `maxConcurrentOperationCount` fixed my problem... Now the count is returned... – Slavcho Dec 20 '16 at 08:25
2

A swift version for an asynchronous operation (which was not very obvious):

final class NetworkOperation: Operation {

lazy var session: NSURLSession = {
        return NSURLSession.sharedSession()
}()

private var _finished = false {

    willSet {
        willChangeValue(forKey: "isFinished")
    }

    didSet {
        didChangeValue(forKey: "isFinished")
    }

}



private var _executing = false {

    willSet {
        willChangeValue(forKey: "isExecuting")
    }

    didSet {
        didChangeValue(forKey: "isExecuting")
    }

}


override var isAsynchronous: Bool {
    return true
}


override var isFinished: Bool {
    return _finished
}


override var isExecuting: Bool {
    return _executing
}

override func start() {
    _executing = true
    execute()
}


func execute() {
    task = session.downloadTaskWithURL(NSURL(string: "yourURL")!) {
        (url, response, error) in

        if error == nil {

            // Notify the response by means of a closure or what you prefer
            // Remember to run in the main thread since NSURLSession runs its
            // task on background by default

        } else {

            // Notify the failure by means of a closure or what you prefer
            // Remember to run in the main thread since NSURLSession runs its
            // task on background by default

        }

        // Remember to tell the operation queue that the execution has completed
        self.finish()
    }

}

func finish() {
    //Async task complete and hence the operation is complete
    _executing = false
    _finished = true
}


}

To serialize the operations:

let operationQueue = OperationQueue()

let operation1 = NetworkOperation()
let operation2 = NetworkOperation()

operation2.addDependency(operation1)

operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)
jarora
  • 5,384
  • 2
  • 34
  • 46
0

Is it manadatory to use NSOperationQueue?

This behaviour is very easy to implement with serial queues

https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html

Assuming you have a class to manage the operations, you would create a serial dispatch queue in your init method with

queue = dispatch_queue_create("com.example.MyQueue", NULL);

And you would have a method to enqueue request, something like this

- (void) enqueueRequest:(NSURL *)requestURL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_sync(queue, ^{ /* get data from requestURL */ }) });
}

This way, only one request is active at one time, even though each request will be executed in a separated background thread, and several requests will be enqueued until the active request finishes.

Jose Servet
  • 469
  • 4
  • 7
  • Yes, the network request would be synchronous to keep it simple, the dispatch_async call would take care of starting a new thread to avoid blocking the main thread – Jose Servet Jul 24 '14 at 16:34