15

I have those methods to retrieve some object information from the internet:

- (void)downloadAppInfo:(void(^)())success
                failure:(void(^)(NSError *error))failure;
- (void)getAvailableHosts:(void(^)())success
                  failure:(void(^)(NSError *error))failure;
- (void)getAvailableServices:(void(^)())success
                     failure:(void(^)(NSError *error))failure;
- (void)getAvailableActions:(void(^)())success
                    failure:(void(^)(NSError *error))failure;

The downloaded stuff gets stored in object properties, so that is why the success functions return nothing.

Now, I want to have one method like this:

- (void)syncEverything:(void(^)())success
               failure:(void(^)(NSError *error))failure;

Which does nothing else than calling all the methods above, and returning only after every single method has performed its success or failure block.

How can I do this?

Hint: I am aware that cascading the methods calls in each others success block would work. But this is neither 'clean' nor helpful when later implementations include further methods.

Attempts:

I tried running each of the calls in an NSOperation and adding those NSOperations to an NSOperationQueue followed by a "completion operation" which depends on every one of the preceding operations.

This won't work. Since the operations are considered completed even before their respective success/failure blocks return.

I also tried using dispatch_group. But it is not clear to me wether I am doing it the right way. Unfortunately, it is not working.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Hasib Samad
  • 1,081
  • 1
  • 20
  • 39
  • 2
    Add each operation to a dispatch group and then send your final block at the end. – Abizern Sep 24 '13 at 14:59
  • 1
    you can take a look at the apple docus for dispatch groups: https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW25 – CarlJ Sep 24 '13 at 15:07
  • Interesting. I wasn't aware of this dispatch group. I'll take a look, but it looks very promising. – Hasib Samad Sep 24 '13 at 15:14
  • Didn't help apparently. See my edit. – Hasib Samad Sep 24 '13 at 15:47
  • @H.A.Samad re: dispatch groups, I [added an answer below](http://stackoverflow.com/a/36608303/1265393) that uses them. – pkamb Oct 11 '16 at 17:24

5 Answers5

18

Drawn from the comments in other answers here, and the blog post Using dispatch groups to wait for multiple web services, I arrived at the following answer.

This solution uses dispatch_group_enter and dispatch_group_leave to determine when each intermediate task is running. When all tasks have finished, the final dispatch_group_notify block is called. You can then call your completion block, knowing that all intermediate tasks have finished.

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
[self yourBlockTaskWithCompletion:^(NSString *blockString) {

    // ...

    dispatch_group_leave(group);
}];

dispatch_group_enter(group);
[self yourBlockTaskWithCompletion:^(NSString *blockString) {

    // ...

    dispatch_group_leave(group);
}];

dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{

    // All group blocks have now completed

    if (completion) {
        completion();
    }
});

Grand Central Dispatch - Dispatch Groups

https://developer.apple.com/documentation/dispatch/dispatchgroup

Grouping blocks allows for aggregate synchronization. Your application can submit multiple blocks and track when they all complete, even though they might run on different queues. This behavior can be helpful when progress can’t be made until all of the specified tasks are complete.

Xcode Snippet:

I find myself using Dispatch Groups enough that I've added the following code as an Xcode Snippet for easy insertion into my code.

Now I type DISPATCH_SET and the following code is inserted. You then copy and paste an enter/leave for each of your async blocks.

Objective-C:

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);

dispatch_group_leave(group);

dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{

});

Swift:

let dispatchGroup = DispatchGroup()

dispatchGroup.enter()

dispatchGroup.leave()

dispatchGroup.notify(queue: .global()) {

}
pkamb
  • 33,281
  • 23
  • 160
  • 191
  • This works in cases where you don't have to makes the calls serially. If you want to wait for first call to return before proceeding to second, and so on... This will not do. – Inn0vative1 May 19 '17 at 15:23
  • Nice and clean! Do you have to worry about a race condition though between the first dispatch_group_enter / dispatch_group_leave pair and the second pair? In other words, is it possible for the dispatch_group_notify to fire for completion if the first block ends before the second block even starts? If so, one fix would be to make all the dispatch_group_enter calls before it is possible for any "leave" calls to fire. – software evolved Dec 05 '17 at 23:07
  • @softwareevolved The `dispatch_group_notify` line comes *below* both calls to `dispatch_group_enter`, so the `notify` block doesn't exist and thus can't be called after only the *first* `enter`. I think the race condition you're proposing would be valid, though, if you wrote the `notify` up top and then all the `enter`/`leave` pairs below. And that would be legal code as far as I can see. – pkamb Dec 06 '17 at 00:08
  • Another way to ensure no race condition would be to do a group enter immediately after creating the group, do various tasks (which also include enter/leave) then after the notify, do a leave. Roughly analogous to the old retain/release code that was used to guard variables. – software evolved Dec 06 '17 at 04:07
10

You were almost there, the problem is most likely to be that those methods are asynchronous, so you need an extra synchronization step. Just try with the following fix:

for(Appliance *appliance in _mutAppliances) {
  dispatch_group_async(
     group,
     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       dispatch_semaphore_t sem = dispatch_semaphore_create( 0 );

       NSLog(@"Block START");

       [appliance downloadAppInfo:^{
          NSLog(@"Block SUCCESS");
            dispatch_semaphore_signal(sem);
       }
       failure:^(NSError *error){
         NSLog(@"Block FAILURE");
         dispatch_semaphore_signal(sem);
       }];

       dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

       NSLog(@"Block END");
 });

 dispatch_group_notify(
   group,
   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
     NSLog(@"FINAL block");
     success();
 });
}
Alex Mitchell
  • 390
  • 1
  • 3
  • 13
micantox
  • 5,446
  • 2
  • 23
  • 27
  • Works like a charm. I better read some docs about the whole GCD thing. One question though, can this by any means be achieved using NSOperations instead of GCD? – Hasib Samad Sep 24 '13 at 16:33
  • Yea, it does get a bit more complicated though. I don't really have time to reply now, if you really want to know how to do it with operation queues, maybe try opening another question? :) – micantox Sep 24 '13 at 16:46
  • 8
    Instead of using dispatch_semaphore_wait() to block a thread I would recommend using dispatch_group_enter() and dispatch_group_leave() for the same group that you are already in to "extend" the group to the completion callback, that way everything is fully asynchronous. – das Sep 24 '13 at 21:02
  • Sure, but he would have to write that code *inside* the `downloadAppInfo:`'s method's body. And we don't know whether he's got access to that code or not. – micantox Sep 24 '13 at 22:34
  • 6
    all I meant is to put dispatch_group_enter() where you have dispatch_semaphore_create() and dispatch_group_leave() where you have dispatch_semaphore_signal() and remove dispatch_sempahore_wait(). If one of these is possible, so is the other. – das Sep 24 '13 at 23:23
  • 1
    Does using dispatch_group_enter() and dispatch_group_leave() allow you to perform the tasks serially? For example, I want to send a delete request to the server, once that completes, I would then like to perform a delete locally. These must be done one after the other. – NYC Tech Engineer Sep 10 '15 at 20:47
  • how about using the RxCocoa? I think it is better for U to handle the issue – Ryan Chou Aug 13 '16 at 02:42
2

One other solution is to use a Promise which is available in a few third party libraries. I'm the author of RXPromise, which implements the Promises/A+ specification.

But there are at least two other Objective-C implementations.

A Promise represents the eventual result of an asynchronous method or operation:

-(Promise*) doSomethingAsync;

The promise is a complete replacement for the completion handler. Additionally, due to its clear specification and underlaying design, it has some very useful features which make it especially easy to handle rather complex asynchronous problems.

What you need to do first, is to wrap your asynchronous methods with completion handlers into asynchronous methods returning a Promise: (Purposefully, your methods return the eventual result and a potential error in a more convenient completion handler)

For example:

- (RXPromise*) downloadAppInfo {
    RXPromise* promise = [RXPromise new];
    [self downloadAppInfoWithCompletion:^(id result, NSError *error) {
        if (error) {
            [promise rejectWithReason:error];
        } 
        else {
            [promise fulfillWithValue:result];
        }
    }];
    return promise;
}

Here, the original asynchronous method becomes the "resolver" of the promise. A promise can be either fulfilled (success) or rejected (failure) with either specifying the eventual result of the task or the reason of the failure. The promise will then hold the eventual result of the asynchronous operation or method.

Note that the wrapper is an asynchronous method, which returns immediately a promise in a "pending" state.

Finally, you obtain the eventual result by "registering" a success and a failure handler with a then method or property. The few promise libraries around do differ slightly, but basically it may look as follows:

`promise.then( <success-handler>, <error-handler> )`

The Promise/A+ Specification has a minimalistic API. And the above is basically ALL one need for implementing the Promise/A+ spec - and is often sufficient in many simple use cases.

However, sometimes you need bit more - for example the OPs problem, which require to "wait" on a set of asynchronous methods and then do something when all have completed.

Fortunately, the Promise is an ideal basic building block to construct more sophisticated helper methods quite easily.

Many Promise libraries provide utility methods. So for example a method all (or similar) which is an asynchronous method returning a Promise and taking an array of promises as input. The returned promise will be resolved when all operations have been completed, or when one fails. It may look as follows:

First construct an array of promises, and simultaneously starting all asynchronous tasks in parallel:

NSArray* tasks = @[
    [self downloadAppInfo],
    [self getAvailableHosts],
    [self getAvailableServices],
    [self getAvailableActions],
];

Note: here, the tasks are already running (and may complete)!

Now, use a helper method which does exactly what stated above:

RXPromise* finalPromise = [RXPromise all:tasks];

Obtain the final results:

finalPromise.then(^id( results){
    [self doSomethingWithAppInfo:results[0] 
                  availableHosts:results[1] 
               availableServices:results[2]  
                availableActions:results[3]];
    return nil;
},  ^id(NSError* error) {
    NSLog(@"Error %@", error); // some async task failed - log the error
});

Note that either the success or the failure handler will be called when the returned promise will be resolved somehow in the all: method.

The returned promise (finalPromise) will be resolved, when

  1. all tasks succeeded successfully, or when
  2. one task failed

For case 1) the final promise will be resolved with an array which contains the result for each corresponding asynchronous task.

In case 2) the final promise will be resolved with the error of the failing asynchronous task.

(Note: the few available libraries may differ here)

The RXPromise library has some additional features:

Sophisticated cancellation which forwards a cancellation signal in the acyclic graph of promises.

A way to specify a dispatch queue where the handler will run. The queue can be used to synchronize access to shared resources for example, e.g.

self.usersPromise = [self fetchUsers];

self.usersPromise.thenOn(dispatch_get_main_queue(), ^id(id users) {
    self.users = users;
    [self.tableView reloadData];
}, nil);

When compared to other approaches, the dispatch_group solution suffers from the fact that it blocks a thread. This is not quite "asynchronous". It's also quite complex if not impossible to implement cancellation.

The NSOperation solution appears to be a mixed blessing. It may be elegant only if you already have NSOperations, and if you have no completion handlers which you need to take into account when defining the dependencies - otherwise, it becomes cluttered and elaborated.

Another solution, not mentioned so far, is Reactive Cocoa. IMHO, it's an awesome library which lets you solve asynchronous problems of virtually any complexity. However, it has a quite steep learning curve, and may add a lot of code to your app. And I guess, 90% of asynchronous problems you stumble over can be solved with cancelable promises. If you have even more complex problems, so take a look at RAC.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • why did you call it a `promise`? A promise to me means something that is guaranteed. Is that what your thought process was when writing the class name? – Pavan Oct 25 '14 at 09:54
  • 1
    @Pavan It simply follows the convention from the Java Script [Promises/A+ specification](https://github.com/promises-aplus/promises-spec). In Scala for example, this would be a _Future_ which is readonly, and it has an accompanying class _Promise_ used to resolve the future. See also: [Scala Futures and Promises](http://docs.scala-lang.org/overviews/core/futures.html), [wiki futures and promises](http://en.wikipedia.org/wiki/Futures_and_promises). – CouchDeveloper Oct 27 '14 at 06:18
1

If you want to create a block based solution you could do something like

- (void)syncEverything:(void(^)())success failure:(void(^)(NSError *error))failure
{
    __block int numBlocks = 4;
    __block BOOL alreadyFailed = NO;

    void (^subSuccess)(void) = ^(){
        numBlocks-=1;
        if ( numBlocks==0 ) {
            success();
        }
    };
    void (^subFailure)(NSError*) = ^(NSError* error){
        if ( !alreadyFailed ) {
            alreadyFailed = YES;
            failure(error);
        }
    };

    [self downloadAppInfo:subSuccess failure:subFailure];
    [self getAvailableHosts:subSuccess failure:subFailure];
    [self getAvailableServices:subSuccess failure:subFailure];
    [self getAvailableActions:subSuccess failure:subFailure];
}

It's kind of quick and dirty, and you might need to do block copys. If more than one method fails, you will only get one overall failure.

Fattie
  • 27,874
  • 70
  • 431
  • 719
fresidue
  • 888
  • 8
  • 10
0

Here is my solution without any dispatch_group.

+(void)doStuffWithCompletion:(void (^)(void))completion{
    __block NSInteger stuffRemaining = 3;

    void (^dataCompletionBlock)(void) = ^void(void) {
        stuffRemaining--;

        if (!stuffRemaining) {
            completion();
        }
    };

    for (NSInteger i = stuffRemaining-1; i > 0; i--) {
        [self doOtherStuffWithParams:nil completion:^() {
            dataCompletionBlock();
        }];
    }
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Laszlo
  • 2,803
  • 2
  • 28
  • 33