How do I wait for an asynchronously dispatched block to finish?

122,485

Solution 1

Trying to use a dispatch_semaphore. It should look something like this:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert…

    dispatch_semaphore_signal(sema);
}];

if (![NSThread isMainThread]) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
} else {
    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) { 
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; 
    }
}

This should behave correctly even if runSomeLongOperationAndDo: decides that the operation isn't actually long enough to merit threading and runs synchronously instead.

Solution 2

In addition to the semaphore technique covered exhaustively in other answers, we can now use XCTest in Xcode 6 to perform asynchronous tests via XCTestExpectation. This eliminates the need for semaphores when testing asynchronous code. For example:

- (void)testDataTask
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];

    NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
    NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        XCTAssertNil(error, @"dataTaskWithURL error %@", error);

        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
            XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
        }

        XCTAssert(data, @"data nil");

        // do additional tests on the contents of the `data` object here, if you want

        // when all done, Fulfill the expectation

        [expectation fulfill];
    }];
    [task resume];

    [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

For the sake of future readers, while the dispatch semaphore technique is a wonderful technique when absolutely needed, I must confess that I see too many new developers, unfamiliar with good asynchronous programming patterns, gravitate too quickly to semaphores as a general mechanism for making asynchronous routines behave synchronously. Worse I've seen many of them use this semaphore technique from the main queue (and we should never block the main queue in production apps).

I know this isn't the case here (when this question was posted, there wasn't a nice tool like XCTestExpectation; also, in these testing suites, we must ensure the test does not finish until the asynchronous call is done). This is one of those rare situations where the semaphore technique for blocking the main thread might be necessary.

So with my apologies to the author of this original question, for whom the semaphore technique is sound, I write this warning to all of those new developers who see this semaphore technique and consider applying it in their code as a general approach for dealing with asynchronous methods: Be forewarned that nine times out of ten, the semaphore technique is not the best approach when encounting asynchronous operations. Instead, familiarize yourself with completion block/closure patterns, as well as delegate-protocol patterns and notifications. These are often much better ways of dealing with asynchronous tasks, rather than using semaphores to make them behave synchronously. Usually there are good reasons that asynchronous tasks were designed to behave asynchronously, so use the right asynchronous pattern rather than trying to make them behave synchronously.

Solution 3

I’ve recently come to this issue again and wrote the following category on NSObject:

@implementation NSObject (Testing)

- (void) performSelector: (SEL) selector
    withBlockingCallback: (dispatch_block_t) block
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self performSelector:selector withObject:^{
        if (block) block();
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_release(semaphore);
}

@end

This way I can easily turn asynchronous call with a callback into a synchronous one in tests:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert…
}];

Solution 4

Generally don't use any of these answers, they often won't scale (there's exceptions here and there, sure)

These approaches are incompatible with how GCD is intended to work and will end up either causing deadlocks and/or killing the battery by nonstop polling.

In other words, rearrange your code so that there is no synchronous waiting for a result, but instead deal with a result being notified of change of state (eg callbacks/delegate protocols, being available, going away, errors, etc.). (These can be refactored into blocks if you don't like callback hell.) Because this is how to expose real behavior to the rest of the app than hide it behind a false façade.

Instead, use NSNotificationCenter, define a custom delegate protocol with callbacks for your class. And if you don't like mucking with delegate callbacks all over, wrap them into a concrete proxy class that implements the custom protocol and saves the various block in properties. Probably also provide convenience constructors as well.

The initial work is slightly more but it will reduce the number of awful race-conditions and battery-murdering polling in the long-run.

(Don't ask for an example, because it's trivial and we had to invest the time to learn objective-c basics too.)

Solution 5

Here's a nifty trick that doesn't use a semaphore:

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
    [object doSomething];
});
dispatch_sync(serialQ, ^{ });

What you do is wait using dispatch_sync with an empty block to Synchronously wait on a serial dispatch queue until the A-Synchronous block has completed.

Share:
122,485

Related videos on Youtube

zoul
Author by

zoul

Updated on April 11, 2020

Comments

  • zoul
    zoul about 4 years

    I am testing some code that does asynchronous processing using Grand Central Dispatch. The testing code looks like this:

    [object runSomeLongOperationAndDo:^{
        STAssert…
    }];
    

    The tests have to wait for the operation to finish. My current solution looks like this:

    __block BOOL finished = NO;
    [object runSomeLongOperationAndDo:^{
        STAssert…
        finished = YES;
    }];
    while (!finished);
    

    Which looks a bit crude, do you know a better way? I could expose the queue and then block by calling dispatch_sync:

    [object runSomeLongOperationAndDo:^{
        STAssert…
    }];
    dispatch_sync(object.queue, ^{});
    

    …but that’s maybe exposing too much on the object.

  • nicktmro
    nicktmro about 12 years
    This code did not work for me. My STAssert would never execute. I had to replace the dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); with while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
  • zoul
    zoul about 12 years
    That’s probably because your completion block is dispatched to the main queue? The queue is blocked waiting for the semaphore and therefore never executes the block. See this question about dispatching on the main queue without blocking.
  • NSCry
    NSCry over 11 years
    I followed the suggestion of @Zoul &nicktmro. But it's looking that it's going to deadlock state. Test Case '-[BlockTestTest testAsync]' started. but never ended
  • Jonesopolis
    Jonesopolis about 11 years
    same for me following zoul's help
  • Peter Warbo
    Peter Warbo about 11 years
    Do you need to release the semaphore under ARC?
  • Patrick
    Patrick about 11 years
    There is an error in the above code. From the NSCondition documentation for -waitUntilDate: "You must lock the receiver prior to calling this method." So the -unlock should be after -waitUntilDate:.
  • Hulvej
    Hulvej almost 11 years
    this was exactly what I was looking for. Thanks! @PeterWarbo no you don't. The use of ARC removes the need to do a dispatch_release()
  • IluTov
    IluTov almost 11 years
    @zoul Does that mean the method nicktmro suggested is not reliable?
  • zoul
    zoul almost 11 years
    It is, @NSAddict, provided that you don’t dispatch anything on the main queue before you signal the semaphore.
  • IluTov
    IluTov almost 11 years
    @zoul But that's the point. The framework I'm using is automatically dispatching back to the main queue as it's work is done. When I'm doing what nicktmro wrote it's all working fine, when I'm using the solution from the answer, the block which is dispatched on the main queue is never executed. What do you recommend?
  • zoul
    zoul almost 11 years
    In that case I would use the busy wait solution, ie. spin the run loop as nicktmro suggests. You don’t have to bother with a semaphore, a simple boolean flag will do (as shown in the question).
  • kevin
    kevin over 10 years
    well, it causes high cpu usage though
  • Admin
    Admin almost 10 years
    @kevin Yup, this is ghetto polling that will murder the battery.
  • Admin
    Admin almost 10 years
    This doesn't scale to anything that uses multiple threads or run queues.
  • Admin
    Admin almost 10 years
    Same problem: battery life fail.
  • Khulja Sim Sim
    Khulja Sim Sim almost 10 years
    @Barry Not sure even if you looked at the code. There is TIMEOUT_SECONDS period within which if async call doesn't respond, it will break the loop. That's the hack to break the deadlock. This code perfectly works without killing the battery.
  • hris.to
    hris.to over 9 years
    I think this should be the accepted answer now. Here are the docs also: developer.apple.com/library/prerelease/ios/documentation/…
  • Benjohn
    Benjohn about 9 years
    I've a question about this. I've got some asynchronous code that performs about a dozen AFNetworking download calls to download a single document. I'd like to schedule downloads on an NSOperationQueue. Unless I use something like a semaphore, the document download NSOperations will all immediately appear to complete and there won't be any real queueing of downloads – they'll pretty much proceed concurrently, which I don't want. Are semaphores reasonable here? Or is there a better way to make NSOperations wait for the asynchronous end of others? Or something else?
  • Rob
    Rob about 9 years
    No, don't use semaphores in this situation. If you have operation queue to which you are adding the AFHTTPRequestOperation objects, then you should then just create a completion operation (which you'll make dependent upon the other operations). Or use dispatch groups. BTW, you say you don't want them running concurrently, which is fine if that's what you need, but you pay serious performance penalty doing this sequentially rather than concurrently. I generally use maxConcurrentOperationCount of 4 or 5.
  • BootMaker
    BootMaker over 8 years
    It's an important warning because of obj-C design patterns and testability, too
  • Jeremy Wiebe
    Jeremy Wiebe about 8 years
    Using DISPATCH_TIME_FOREVER is almost never the right solution. You're building in a deadlock should the code in your block ever encounter a situation where it doesn't reach the dispatch_semaphone_signal call.
  • dhruvm
    dhruvm almost 8 years
    @nicktmro You should make your comment one of the answers!
  • Ryan Chou
    Ryan Chou almost 8 years
    ``` dispatch_release(semaphore);``` seems wrong. It notified me release is not available not available in automatic reference couting mode. Is there something wrong with your code?
  • BTRUE
    BTRUE about 7 years
    The problem with this answer is that it doesn't address the OP's original problem, which is that the API that needs to be used takes a completionHandler as an argument and returns immediately. Calling that API inside of this answer's async block would return immediately even though the completionHandler had not run yet. Then the sync block would be executing before the completionHandler.
  • pkc456
    pkc456 almost 7 years
    @Barry, how does it consume more battery. please guide.
  • Admin
    Admin about 6 years
    @pkc456 Have a look in a computer science book about the differences between how polling and asynchronous notification work. Good luck.
  • Admin
    Admin almost 6 years
    Four and a half years later and with the knowledge and experience I've gained I would not recommend my answer.
  • Zohar81
    Zohar81 almost 6 years
    Hi, Do you know if it's possible to use GCD to implement signal and wait between 2 different processes in macOS ?
  • marcelosalloum
    marcelosalloum about 5 years
    Optionally, you can call dispatch_semaphore_create(1); instead of dispatch_semaphore_create(0) and place the dispatch_semaphore_wait command above the block. This approach worked better for my case. It's similar to a do/while(condition). The answer above, on the other hand, is similar to while(condition)/do
  • ikel
    ikel almost 5 years
    ARC forbids explicit message send of 'release'
  • Albert Renshaw
    Albert Renshaw over 4 years
    @nicktmro Do you recall why you use dateWithTimeIntervalSinceNow:10 specifically? I've found it still seems to work with using 0 instead of 10 and was wondering if you had a strategic reason for using 10?
  • Albert Renshaw
    Albert Renshaw over 4 years
    Based on the many comments here, and the age of this answer, I've updated it to use Kperryua's original solution when not on the main thread, but nicktmro's solution when on the main thread, as well as wrapping the memory management code in ARC detecting CLANG since that's mostly now obsolete.