Comment attendre la fin d'un bloc distribué de manière asynchrone?


182

Je teste du code qui effectue un traitement asynchrone à l'aide de Grand Central Dispatch. Le code de test ressemble à ceci:

[object runSomeLongOperationAndDo:^{
    STAssert…
}];

Les tests doivent attendre la fin de l'opération. Ma solution actuelle ressemble à ceci:

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

Ce qui a l'air un peu grossier, connaissez-vous un meilleur moyen? Je pourrais exposer la file d'attente puis la bloquer en appelant dispatch_sync:

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

… Mais c'est peut-être trop exposer sur le object.

Réponses:


304

Essayer d'utiliser un fichier dispatch_semaphore. Ça devrait ressembler a quelque chose comme ca:

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]]; 
    }
}

Cela devrait se comporter correctement même si cela runSomeLongOperationAndDo:décide que l'opération n'est pas suffisamment longue pour mériter un thread et s'exécute de manière synchrone à la place.


61
Ce code n'a pas fonctionné pour moi. Mon STAssert ne s'exécuterait jamais. J'ai dû remplacer le dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);parwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
C'est probablement parce que votre bloc d'achèvement est envoyé dans la file d'attente principale? La file d'attente est bloquée en attendant le sémaphore et n'exécute donc jamais le bloc. Voir cette question sur la répartition sur la file d'attente principale sans blocage.
zoul

3
J'ai suivi la suggestion de @Zoul & nicktmro. Mais il semble qu'il va dans une impasse. Le scénario de test '- [BlockTestTest testAsync]' a démarré. mais jamais terminé
NSCry

3
Avez-vous besoin de libérer le sémaphore sous ARC?
Peter Warbo

14
c'était exactement ce que je cherchais. Merci! @PeterWarbo non vous ne le faites pas. L'utilisation d'ARC supprime le besoin de faire un dispatch_release ()
Hulvej

31

En plus de la technique du sémaphore abordée de manière exhaustive dans d'autres réponses, nous pouvons désormais utiliser XCTest dans Xcode 6 pour effectuer des tests asynchrones via XCTestExpectation. Cela élimine le besoin de sémaphores lors du test de code asynchrone. Par exemple:

- (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.


1
I think this should be the accepted answer now. Here are the docs also: developer.apple.com/library/prerelease/ios/documentation/…
hris.to

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?
Benjohn

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.
Rob

29

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…
}];

24

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.)


1
It's an important warning because of obj-C design patterns and testability, too
BootMaker

8

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.


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.
BTRUE

6
- (void)performAndWait:(void (^)(dispatch_semaphore_t semaphore))perform;
{
  NSParameterAssert(perform);
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  perform(semaphore);
  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  dispatch_release(semaphore);
}

Example usage:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];

2

There’s also SenTestingKitAsync that lets you write code like this:

- (void)testAdditionAsync {
    [Calculator add:2 to:2 block^(int result) {
        STAssertEquals(result, 4, nil);
        STSuccess();
    }];
    STFailAfter(2.0, @"Timeout");
}

(See objc.io article for details.) And since Xcode 6 there’s an AsynchronousTesting category on XCTest that lets you write code like this:

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];

1

Here is an alternative from one of my tests:

__block BOOL success;
NSCondition *completed = NSCondition.new;
[completed lock];

STAssertNoThrow([self.client asyncSomethingWithCompletionHandler:^(id value) {
    success = value != nil;
    [completed lock];
    [completed signal];
    [completed unlock];
}], nil);    
[completed waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[completed unlock];
STAssertTrue(success, nil);

1
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:.
Patrick

This doesn't scale to anything that uses multiple threads or run queues.

0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object blockToExecute:^{
    // ... your code to execute
    dispatch_semaphore_signal(sema);
}];

while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
    [[NSRunLoop currentRunLoop]
        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}

This did it for me.


3
well, it causes high cpu usage though
kevin

4
@kevin Yup, this is ghetto polling that will murder the battery.

@Barry, how does it consume more battery. please guide.
pkc456

@pkc456 Have a look in a computer science book about the differences between how polling and asynchronous notification work. Good luck.

2
Four and a half years later and with the knowledge and experience I've gained I would not recommend my answer.

0

Sometimes, Timeout loops are also helpful. May you wait until you get some (may be BOOL) signal from async callback method, but what if no response ever, and you want to break out of that loop? Here below is solution, mostly answered above, but with an addition of Timeout.

#define CONNECTION_TIMEOUT_SECONDS      10.0
#define CONNECTION_CHECK_INTERVAL       1

NSTimer * timer;
BOOL timeout;

CCSensorRead * sensorRead ;

- (void)testSensorReadConnection
{
    [self startTimeoutTimer];

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {

        /* Either you get some signal from async callback or timeout, whichever occurs first will break the loop */
        if (sensorRead.isConnected || timeout)
            dispatch_semaphore_signal(sema);

        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:CONNECTION_CHECK_INTERVAL]];

    };

    [self stopTimeoutTimer];

    if (timeout)
        NSLog(@"No Sensor device found in %f seconds", CONNECTION_TIMEOUT_SECONDS);

}

-(void) startTimeoutTimer {

    timeout = NO;

    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:CONNECTION_TIMEOUT_SECONDS target:self selector:@selector(connectionTimeout) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void) stopTimeoutTimer {
    [timer invalidate];
    timer = nil;
}

-(void) connectionTimeout {
    timeout = YES;

    [self stopTimeoutTimer];
}

1
Same problem: battery life fail.

1
@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.
Khulja Sim Sim

0

Very primitive solution to the problem:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert…
    nextOperationAfterLongOperationBlock();
}];

0

Swift 4:

Use synchronousRemoteObjectProxyWithErrorHandler instead of remoteObjectProxy when creating the remote object. No more need for a semaphore.

Below example will return the version received from the proxy. Without the synchronousRemoteObjectProxyWithErrorHandler it will crash (trying to access non accessible memory):

func getVersion(xpc: NSXPCConnection) -> String
{
    var version = ""
    if let helper = xpc.synchronousRemoteObjectProxyWithErrorHandler({ error in NSLog(error.localizedDescription) }) as? HelperProtocol
    {
        helper.getVersion(reply: {
            installedVersion in
            print("Helper: Installed Version => \(installedVersion)")
            version = installedVersion
        })
    }
    return version
}

-1

I have to wait until a UIWebView is loaded before running my method, I was able to get this working by performing UIWebView ready checks on main thread using GCD in combination with semaphore methods mentioned in this thread. Final code looks like this:

-(void)myMethod {

    if (![self isWebViewLoaded]) {

            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

            __block BOOL isWebViewLoaded = NO;

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                while (!isWebViewLoaded) {

                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        isWebViewLoaded = [self isWebViewLoaded];
                    });

                    [NSThread sleepForTimeInterval:0.1];//check again if it's loaded every 0.1s

                }

                dispatch_sync(dispatch_get_main_queue(), ^{
                    dispatch_semaphore_signal(semaphore);
                });

            });

            while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
            }

        }

    }

    //Run rest of method here after web view is loaded

}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.