meaning-matters
meaning-matters

Reputation: 22936

How to wait for method that has completion block (all on main thread)?

I have the following (pseudo) code:

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        for (NSString name in names)
        {
            [someObject lookupName:name completion:^(NSString* urlString)
            {
                // A. Something that takes a few seconds to complete.
            }];

            // B. Need to wait here until A is completed.
        }
    }];

    // C. Need to wait here until all iterations above have finished.
    STAssertTrue(...);
}

This code is running on main thread, and also the completion block A is on main thread.

Upvotes: 21

Views: 37495

Answers (7)

Tricertops
Tricertops

Reputation: 8512

If your completion block is also called on the Main Thread, it might be difficult to achieve this, because before the completion block can execute, your method need to return. You should change implementation of the asynchronous method to:

  1. Be synchronous.
    or
  2. Use other thread/queue for completion. Then you can use Dispatch Semaphores for waiting. You initialize a semaphore with value 0, then call wait on main thread and signal in completion.

In any case, blocking Main Thread is very bad idea in GUI applications, but that wasn't part of your question. Blocking Main Thread may be required in tests, in command-line tools, or other special cases. In that case, read further:


How to wait for Main Thread callback on the Main Thread:

There is a way to do it, but could have unexpected consequences. Proceed with caution!

Main Thread is special. It runs +[NSRunLoop mainRunLoop] which handles also +[NSOperationQueue mainQueue] and dispatch_get_main_queue(). All operations or blocks dispatched to these queues will be executed within the Main Run Loop. This means, that the methods may take any approach to scheduling the completion block, this should work in all those cases. Here it is:

__block BOOL isRunLoopNested = NO;
__block BOOL isOperationCompleted = NO;
NSLog(@"Start");
[self performOperationWithCompletionOnMainQueue:^{
    NSLog(@"Completed!");
    isOperationCompleted = YES;
    if (isRunLoopNested) {
        CFRunLoopStop(CFRunLoopGetCurrent()); // CFRunLoopRun() returns
    }
}];
if ( ! isOperationCompleted) {
    isRunLoopNested = YES;
    NSLog(@"Waiting...");
    CFRunLoopRun(); // Magic!
    isRunLoopNested = NO;
}
NSLog(@"Continue");

Those two booleans are to ensure consistency in case of the block finished synchronously immediately.

In case the -performOperationWithCompletionOnMainQueue: is asynchronous, the output would be:

Start
Waiting...
Completed!
Continue

In case the method is synchronous, the output would be:

Start
Completed!
Continue

What is the Magic? Calling CFRunLoopRun() doesn’t return immediately, but only when CFRunLoopStop() is called. This code is on Main RunLoop so running the Main RunLoop again will resume execution of all scheduled block, timers, sockets and so on.

Warning: The possible problem is, that all other scheduled timers and block will be executed in meantime. Also, if the completion block is never called, your code will never reach Continue log.

You could wrap this logic in an object, that would make easier to use this pattern repeatedy:

@interface MYRunLoopSemaphore : NSObject

- (BOOL)wait;
- (BOOL)signal;

@end

So the code would be simplified to this:

MYRunLoopSemaphore *semaphore = [MYRunLoopSemaphore new];
[self performOperationWithCompletionOnMainQueue:^{
    [semaphore signal];
}];
[semaphore wait];

Upvotes: 23

Paul Fennema
Paul Fennema

Reputation: 305

I think that Mike Ash (http://www.mikeash.com/pyblog/friday-qa-2013-08-16-lets-build-dispatch-groups.html has exactly the answer to 'waiting for several threads on completion and then do something when all threads are finished'. The nice thing is that you even can wait either synchronously or a-synchronously, using dispatch groups.

A short example copied and modified from Mike Ash his blog:

    dispatch_group_t group = dispatch_group_create();

    for(int i = 0; i < 100; i++)
    {
        dispatch_group_enter(group);
        DoAsyncWorkWithCompletionBlock(^{
            // Async work has been completed, this must be executed on a different thread than the main thread

            dispatch_group_leave(group);
        });
    }

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

Alternatively, you can a-synchronously wait and perform an action when all blocks completed instead of the dispatch_group_wait:

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    UpdateUI();
});

Upvotes: 3

dpassage
dpassage

Reputation: 5453

Lots of good general-purpose answers above - but it looks like what you're trying to do is write a unit test for a method that uses a completion block. You don't know if the test has passed until the block is called, which happens asynchronously.

In my current project, I'm using SenTestingKitAsync to do this. It extends OCTest so that after all the tests are run, it executes whatever's waiting on the main run loop and evaluates those assertions as well. So your test could look like:

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        STSuccess();
    }];

    STFailAfter(500, @"block should have been called");
}

I would also recommend testing someThing and someObject in two separate tests, but that's regardless of the asynchronous nature of what you're testing.

Upvotes: 1

CouchDeveloper
CouchDeveloper

Reputation: 19098

I'm currently developing a library (RXPromise, whose sources are on GitHub) which makes a number of complex asynchronous patterns quite easy to implement.

The following approach utilizes a class RXPromise and yields code which is 100% asynchronous - which means, there is absolutely no blocking. "waiting" will be accomplished through the handlers which get called when an asynchronous tasks is finished or cancelled.

It also utilizes a category for NSArray which is not part of the library - but can be easily implemented utilizing RXPromise library.

For example, your code could then look like this:

- (RXPromise*)asyncTestAbc
{
    return [someThing retrieve:@"foo"]
    .then(^id(id unused /*names?*/) {
        // retrieve:@"foo" finished with success, now execute this on private queue:
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        return [names rx_serialForEach:^RXPromise* (id name) { /* return eventual result when array finished */
            return [someObject lookupName:name] /* return eventual result of lookup's completion handler */
            .thenOn(mainQueue, ^id(id result) {
                assert(<we are on main thread>);
                // A. Do something after a lookupName:name completes a few seconds later
                return nil;
            }, nil /*might be implemented to detect a cancellation and "backward" it to the lookup task */);
        }]
    },nil);
}

In order to test the final result:

[self asyncTestAbc]
.thenOn(mainQueue, ^id(id result) {
    // C. all `[someObject lookupName:name]` and all the completion handlers for
    // lookupName,  and `[someThing retrieve:@"foo"]` have finished.
    assert(<we are on main thread>);
    STAssertTrue(...);
}, id(NSError* error) {
    assert(<we are on main thread>);
    STFail(@"ERROR: %@", error);
});

The method asyncTestABC will exactly do what you have described - except that it's asynchronous. For testing purposes you can wait until it completes:

  [[self asyncTestAbc].thenOn(...) wait];

However, you must not wait on the main thread, otherwise you get a deadlock since asyncTestAbc invokes completion handler on the main thread, too.


Please request a more detailed explanation if you find this useful!


Note: the RXPromise library is still "work under progress". It may help everybody dealing with complex asynchronous patterns. The code above uses a feature not currently committed to master on GitHub: Property thenOn where a queue can be specified where handlers will be executed. Currently there is only property then which omits the parameter queue where handler shall run. Unless otherwise specified all handler run on a shared private queue. Suggestions are welcome!

Upvotes: 2

Leonard Pauli
Leonard Pauli

Reputation: 2673

It's often a bad approach to block the main thread, it will just make your app unresponsive, so why not do something like this instead?

NSArray *names;
int namesIndex = 0;
- (void)setup {

    // Insert code for adding loading animation

    [UIView animateWithDuration:1 animations:^{
        self.view.alpha = self.view.alpha==1?0:1;
    } completion:^(BOOL finished) {
        names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        [self alterNames];
    }];
}

- (void)alterNames {

    if (namesIndex>=names.count) {
        // Insert code for removing loading animation
        // C. Need to wait here until all iterations above have finished.
        return;
    }


    NSString *name = [names objectAtIndex:namesIndex];
    [UIView animateWithDuration:1 animations:^{
        self.view.alpha = self.view.alpha==1?0:1;
    } completion:^(BOOL finished) {
        name = @"saf";
        // A. Something that takes a few seconds to complete.
        // B. Need to wait here until A is completed.

        namesIndex++;
        [self alterNames];
    }];

}

I have just used [UIView animation...] to make to example fully functional. Just copy and paste into your viewcontroller.m and call [self setup]; Of course, you should replace that with your code.

Or if you want:

NSArray *names;
int namesIndex = 0;
- (void)setup {

    // Code for adding loading animation

    [someThing retrieve:@"foo" completion:^ {
        names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        [self alterNames];
    }];
}

- (void)alterNames {

    if (namesIndex>=names.count) {
        // Code for removing loading animation
        // C. Need to wait here until all iterations above have finished.
        return;
    }

    NSString *name = [names objectAtIndex:namesIndex];
    [someObject lookupName:name completion:^(NSString* urlString) {
        name = @"saf";
        // A. Something that takes a few seconds to complete.
        // B. Need to wait here until A is completed.

        namesIndex++;
        [self alterNames];
    }];

}

Explanation:

  1. Start everything by calling [self setup];
  2. A block will be called when someThing retrieves "foo", in other words, it will wait until someThing retrieves "foo" (and the main thread won't be blocked)
  3. When the block is executed, alterNames is called
  4. If all the items in "names" have been looped through, the "looping" will stop and C could be executed.
  5. Else, lookup the name, and when it's done, do something with it (A), and because it happens on the main thread (You haven't said anything else), you could do B there too.
  6. So, when A and B is complete, jump back to 3

See?

Good luck with your project!

Upvotes: 1

arundevma
arundevma

Reputation: 933

 Move B and C to two methods.

int flagForC = 0, flagForB = 0;
     [someThing retrieve:@"foo" completion:^
    {
        flagForC++;
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        for (NSString name in names)
        {
            [someObject lookupName:name completion:^(NSString* urlString)
            {
                // A. Something that takes a few seconds to complete.
               flagForB++;

               if (flagForB == [names Count])
               {
                   flagForB = 0;
                   //call B
                    if (flagForC == thresholdCount)
                    {
                          flagForC = 0;
                         //Call C 
                    }
               }
            }];


        }
    }];

Upvotes: 0

johnyu
johnyu

Reputation: 2151

int i = 0;
//the below code goes instead of for loop
NSString *name = [names objectAtIndex:i];

[someObject lookupName:name completion:^(NSString* urlString)
{
    // A. Something that takes a few seconds to complete.
    // B.
    i+= 1;
    [self doSomethingWithObjectInArray:names atIndex:i];


}];




/* add this method to your class */
-(void)doSomethingWithObjectInArray:(NSArray*)names atIndex:(int)i {
    if (i == names.count) {
        // C.
    }
    else {
        NSString *nextName = [names objectAtIndex:i];
        [someObject lookupName:nextName completion:^(NSString* urlString)
        {
            // A. Something that takes a few seconds to complete.
            // B.
            [self doSomethingWithObjectInArray:names atIndex:i+1];
        }];
    }
}

I just typed the code here, so some methods names might be spelled wrong.

Upvotes: 2

Related Questions