20

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?
meaning-matters
  • 21,929
  • 10
  • 82
  • 142

7 Answers7

23

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];
Tricertops
  • 8,492
  • 1
  • 39
  • 41
  • 3
    Waiting for something to be finished is what the completion blocks are for. So calling lookupName in it's own completion block to recursively go through every array element should work fine. – johnyu Jul 29 '13 at 13:47
  • No, you're wrong. This should not be accepted. @johnyu is totally right. – Leonard Pauli Jul 29 '13 at 14:23
  • @LeonardPauli If the question asks about _“waiting”_ in the meaning of _“blocking the execution until something happen”_ it is just not possible. – Tricertops Jul 29 '13 at 14:43
  • iMartin probably, but I hope that's not the case, as you point out, too. The same effect that the asker is writing about can be archived with the blocks and functions, as both @johnyu and I write about. Sorry if I went a bit irritated... – Leonard Pauli Jul 29 '13 at 15:01
  • @LeonardPauli I already was in situation, that I had to use existing async calls in plain (non-GUI) program and I had to block the main thread using the semaphores (as I mentioned). So I don't assume anything. – Tricertops Jul 29 '13 at 17:59
  • 1
    The OP's question appears to be from a Unit Test, in which case this is actually possible (it's possible if it's actual code too, just not desirable because of blocking the main thread). The key is to trigger the runloop and wait for a flag which you set in the completion handler. See this answer for details: http://stackoverflow.com/questions/7817605/pattern-for-unit-testing-async-queue-that-calls-main-queue-on-completion – ikuramedia Jun 05 '14 at 11:03
  • @ikuramedia is right wbout NSRunLoop approach. I extended the answer with this solution. – Tricertops Mar 26 '15 at 09:27
  • @iMartin - shouldn't you use `CFRunLoopRun()` instead of `CFRunLoopRun(CFRunLoopGetMain()); // Magic!` because this method doesn't have any attributes? – Apan Jul 09 '15 at 09:40
  • Wish I could give you more points. So frustrating when this question is asked and all the replies are don't block the main thread. Glad I found it, needed to let a webview load synchronously to return the results to the calling function. – Conor Apr 19 '18 at 12:13
  • Seems to work in Swift, not that I recommend using it. – sudo Jun 26 '18 at 20:19
3

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();
});
Paul Fennema
  • 195
  • 2
  • 6
  • 1
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. – laalto Oct 29 '13 at 08:05
  • @laalto, thanks I've updated my answer to include a code example – Paul Fennema Oct 29 '13 at 08:36
  • 1
    Changed my answer, this solution obviously only works when the completion blocks are NOT executed on the main thread – Paul Fennema Oct 29 '13 at 12:10
2
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.

johnyu
  • 2,152
  • 1
  • 15
  • 33
  • Won't work because the loop will fire off all four `lookupName` calls, which take some time. There's no waiting at all here. – meaning-matters Jul 29 '13 at 09:31
  • Does not work, because both `retrieve` and `lookupName` return immediately. – meaning-matters Jul 29 '13 at 11:52
  • There is no "retrieve" in my answer. O.o – johnyu Jul 29 '13 at 11:59
  • If there's no `retrieve` than you've missed something in my code; yet another reason why it won't work. I can't change these methods with blocks, so you can't change the code and leave it out. – meaning-matters Jul 29 '13 at 18:33
  • I have comment in my code that a part of it goes to what you posted, replacing the for loop. And that is inside the retrieve method. If retrieve and lookupName return immediately, that is just because of your implementation of these methods. – johnyu Jul 30 '13 at 10:10
2

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!

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • I would also strongly recommend RXPromise as a library for simplifying asynchronous calls - we use it in all our projects – sam_smith Jun 01 '16 at 01:42
  • 1
    @angryTurtle Thank you for the warm support. Today, RXPromise is in a stable version v1.0.2 and used in more than 1600 projects. – CouchDeveloper Jun 02 '16 at 09:00
1

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!

Leonard Pauli
  • 2,662
  • 1
  • 23
  • 23
  • Weird that you're using a UIView do get a delay; there are much better ways. A delay won't work, because the duration is not defined; and it's simply bad to wait a fixed time. – meaning-matters Jul 29 '13 at 09:58
  • Does not work, because both `retrieve` and `lookupName` return immediately. – meaning-matters Jul 29 '13 at 11:51
  • Have you tried!? It works perfectly fine for me and I can't see anything wrong... Of course, if A also is doing something on another thread, then you'll have to repeat the thing i did again. Basically, re-call the method when done instead of looping. This is working, accepted answer is wrong. I'm using this all the time. – Leonard Pauli Jul 29 '13 at 13:54
  • See my new edit for explanation. It do work! Yes, the methods returns immediately, but that's the whole point with the completion blocks!! They won't execute immediately! – Leonard Pauli Jul 29 '13 at 14:15
  • Please try and don't say it won't work when it really do. My very first answer worked (okay, it had a spelling mistake, butt still). Ahhrg... – Leonard Pauli Jul 29 '13 at 14:20
  • It does not work. I need to execute code at C, just before continuing with the remaining code: in my case calling an iOS test method (see added code in my post). Your code runs through because the methods with block don't wait. I did try, but does NOT work. And as @1Martin correctly says: it can't. – meaning-matters Jul 29 '13 at 18:31
1

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.

dpassage
  • 5,423
  • 3
  • 25
  • 53
  • My `someThing` and `someObject` are both needed before I can start a test; they get something from an external internet source. – meaning-matters Jul 30 '13 at 17:34
  • Thanks for pointing to SenTestingKitAsync; I did not know about it. – meaning-matters Jul 30 '13 at 17:36
  • No problem; lost track of the blog post I found it in. With regards to `someThing` and `someObject`, you might consider mocks to separate the tests between the two. But that then gets into a whole other discussion... – dpassage Jul 30 '13 at 18:12
0
 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 
                    }
               }
            }];


        }
    }];
arundevma
  • 933
  • 7
  • 24