Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

  • How do I wait at B for A to complete?
  • How do subsequently wait at C for the outer completion block to complete?
like image 580
meaning-matters Avatar asked Jul 29 '13 09:07

meaning-matters


3 Answers

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];
like image 182
Tricertops Avatar answered Nov 12 '22 10:11

Tricertops


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.

like image 2
johnyu Avatar answered Nov 12 '22 10:11

johnyu


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!

like image 2
CouchDeveloper Avatar answered Nov 12 '22 11:11

CouchDeveloper