Come posso attendere il completamento di un blocco inviato in modo asincrono?


180

Sto testando del codice che esegue l'elaborazione asincrona utilizzando Grand Central Dispatch. Il codice di test è simile al seguente:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

I test devono attendere il completamento dell'operazione. La mia soluzione attuale si presenta così:

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

Che sembra un po 'rozzo, conosci un modo migliore? Potrei esporre la coda e quindi bloccare chiamando dispatch_sync:

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

... ma forse sta esponendo troppo sul object.

Risposte:


302

Prova di usare a dispatch_semaphore. Dovrebbe assomigliare a qualcosa di simile a questo:

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

Questo dovrebbe comportarsi correttamente anche se runSomeLongOperationAndDo:decide che l'operazione non è effettivamente abbastanza lunga da meritare il threading e viene eseguita invece in modo sincrono.


61
Questo codice non ha funzionato per me. Il mio STAssert non verrà mai eseguito. Ho dovuto sostituire il dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);conwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
Questo probabilmente perché il tuo blocco di completamento viene inviato alla coda principale? La coda viene bloccata in attesa del semaforo e pertanto non esegue mai il blocco. Vedi questa domanda sul dispacciamento nella coda principale senza bloccare.
zoul

3
Ho seguito il suggerimento di @Zoul & nicktmro. Ma sembra che stia andando allo stato di deadlock. Caso di test "- [BlockTestTest testAsync]" avviato. ma non è mai finito
NSCry

3
Devi rilasciare il semaforo sotto ARC?
Peter Warbo,

14
questo era esattamente quello che stavo cercando. Grazie! @PeterWarbo no. L'uso di ARC elimina la necessità di fare un dispatch_release ()
Hulvej

29

Oltre alla tecnica dei semafori trattata esaurientemente in altre risposte, ora possiamo usare XCTest in Xcode 6 per eseguire test asincroni tramite XCTestExpectation. Ciò elimina la necessità di semafori durante il test del codice asincrono. Per esempio:

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

Per il bene dei futuri lettori, mentre la tecnica del semaforo di invio è una tecnica meravigliosa quando assolutamente necessaria, devo confessare che vedo troppi nuovi sviluppatori, che non hanno familiarità con buoni schemi di programmazione asincrona, gravitano troppo rapidamente verso i semafori come meccanismo generale per rendere asincroni le routine si comportano in modo sincrono. Peggio ancora, ho visto molti di loro usare questa tecnica di semaforo dalla coda principale (e non dovremmo mai bloccare la coda principale nelle app di produzione).

So che questo non è il caso qui (quando è stata pubblicata questa domanda, non esisteva uno strumento carino come XCTestExpectation; inoltre, in queste suite di test, dobbiamo assicurarci che il test non finisca fino a quando non viene eseguita la chiamata asincrona). Questa è una di quelle rare situazioni in cui potrebbe essere necessaria la tecnica del semaforo per bloccare il thread principale.

Quindi, con le mie scuse all'autore di questa domanda originale, per la quale la tecnica del semaforo è valida, scrivo questo avvertimento a tutti quei nuovi sviluppatori che vedono questa tecnica del semaforo e considerano l'applicazione nel loro codice come un approccio generale per trattare con asincrono metodi: essere avvisato che nove volte su dieci, la tecnica del semaforo non lo èl'approccio migliore quando si incontrano operazioni asincrone. Invece, familiarizza con i modelli di blocco / chiusura di completamento, nonché i modelli e le notifiche del protocollo delegato. Questi sono spesso modi molto migliori di affrontare compiti asincroni, piuttosto che usare i semafori per farli comportare in modo sincrono. Di solito ci sono buoni motivi per cui le attività asincrone sono state progettate per comportarsi in modo asincrono, quindi utilizzare il modello asincrono giusto anziché cercare di farle comportare in modo sincrono.


1
Penso che questa dovrebbe essere la risposta accettata ora. Ecco anche i documenti: developer.apple.com/library/prerelease/ios/documentation/…
hris.to

Ho una domanda a riguardo. Ho del codice asincrono che esegue circa una dozzina di chiamate di download di AFNetworking per scaricare un singolo documento. Vorrei programmare i download su un NSOperationQueue. A meno che non utilizzi qualcosa come un semaforo, i download dei documenti NSOperationsembreranno immediatamente completati e non ci sarà alcuna vera coda di download - procederanno praticamente contemporaneamente, cosa che non voglio. I semafori sono ragionevoli qui? O c'è un modo migliore per far aspettare NSOperations dalla fine asincrona degli altri? O qualcos'altro?
Benjohn,

No, non usare i semafori in questa situazione. Se si dispone di una coda operazioni a cui si stanno aggiungendo gli AFHTTPRequestOperationoggetti, è necessario creare un'operazione di completamento (che dipenderà dalle altre operazioni). Oppure usa i gruppi di invio. A proposito, dici che non vuoi che funzionino contemporaneamente, il che va bene se è quello che ti serve, ma paghi una grave penalità prestazionale facendo questo in sequenza invece che contemporaneamente. Io generalmente uso maxConcurrentOperationCountdi 4 o 5.
Rob

28

Recentemente sono tornato su questo problema e ho scritto la seguente categoria su 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

In questo modo posso facilmente trasformare una chiamata asincrona con una richiamata in una sincrona nei test:

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

24

Generalmente non usano nessuna di queste risposte, spesso non si ridimensionano (ci sono eccezioni qua e là, certo)

Questi approcci sono incompatibili con il modo in cui GCD è destinato a funzionare e finiranno per causare deadlock e / o uccidere la batteria mediante polling senza sosta.

In altre parole, riorganizza il tuo codice in modo che non ci siano sincroni in attesa di un risultato, ma gestisci invece un risultato che viene notificato del cambio di stato (ad es. Callback / protocolli di delegato, disponibilità, cancellazione, errori, ecc.). (Questi possono essere trasformati in blocchi se non ti piace l'inferno di callback.) Perché questo è come esporre un comportamento reale al resto dell'app piuttosto che nasconderlo dietro una falsa facciata.

Utilizzare invece NSNotificationCenter , definire un protocollo delegato personalizzato con callback per la propria classe. E se non ti piace confondere con i callback delegati dappertutto, avvolgili in una classe proxy concreta che implementa il protocollo personalizzato e salva i vari blocchi nelle proprietà. Probabilmente forniscono anche costruttori di convenienza.

Il lavoro iniziale è leggermente più lungo, ma a lungo termine ridurrà il numero di terribili condizioni di gara e il polling di omicidi di batteria.

(Non chiedere un esempio, perché è banale e abbiamo dovuto investire il tempo per imparare anche le basi dell'obiettivo-c.)


1
È un avvertimento importante anche per i motivi di progettazione e la testabilità di obj-C
BootMaker

8

Ecco un trucco ingegnoso che non usa un semaforo:

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

Quello che fai è aspettare utilizzando dispatch_synccon un blocco vuoto per attendere in modo sincrono su una coda di invio seriale fino al completamento del blocco A-Synchronous.


Il problema con questa risposta è che non affronta il problema originale dell'OP, ovvero che l'API che deve essere utilizzata accetta un completamentoHandler come argomento e restituisce immediatamente. La chiamata dell'API all'interno del blocco asincrono di questa risposta ritornerebbe immediatamente anche se il completamentoHandler non era ancora stato eseguito. Quindi il blocco di sincronizzazione verrà eseguito prima di completamentoHandler.
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);
}

Esempio di utilizzo:

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

2

C'è anche SenTestingKitAsync che ti consente di scrivere codice in questo modo:

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

(Vedi l'articolo objc.io per i dettagli.) E poiché Xcode 6 c'è una AsynchronousTestingcategoria XCTestche ti permette di scrivere codice come questo:

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

1

Ecco un'alternativa a uno dei miei test:

__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
Si è verificato un errore nel codice sopra. Dalla NSCondition documentazione per -waitUntilDate:"È necessario bloccare il ricevitore prima di chiamare questo metodo." Quindi -unlockdovrebbe essere dopo -waitUntilDate:.
Patrick,

Questo non si adatta a nulla che utilizza più thread o esegue code.

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

Questo è stato per me.


3
beh, causa però un elevato utilizzo della CPU
Kevin

4
@kevin Yup, questo è il sondaggio del ghetto che ucciderà la batteria.

@ Barry, come consuma più batteria. per favore guida.
pkc456,

@ pkc456 Dai un'occhiata in un libro di informatica sulle differenze tra il polling e la notifica asincrona. In bocca al lupo.

2
Quattro anni e mezzo dopo e con la conoscenza e l'esperienza acquisite non consiglierei la mia risposta.

0

A volte, sono utili anche i loop di timeout. Potresti aspettare fino a quando non riceverai un segnale (potrebbe essere BOOL) dal metodo di callback asincrono, ma cosa succede se nessuna risposta mai e vuoi uscire da quel ciclo? Di seguito è una soluzione, per lo più con risposta sopra, ma con un'aggiunta di 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
Stesso problema: durata della batteria non riuscita.

1
@ Barry Non sono sicuro anche se hai guardato il codice. C'è un periodo TIMEOUT_SECONDS entro il quale se la chiamata asincrona non risponde, si interromperà il ciclo. Questo è l'hack per rompere lo stallo. Questo codice funziona perfettamente senza uccidere la batteria.
Khulja Sim Sim

0

Soluzione molto primitiva al problema:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

0

Swift 4:

Utilizzare synchronousRemoteObjectProxyWithErrorHandlerinvece di remoteObjectProxyquando si crea l'oggetto remoto. Non c'è più bisogno di un semaforo.

Nell'esempio seguente verrà restituita la versione ricevuta dal proxy. Senza questo synchronousRemoteObjectProxyWithErrorHandlersi bloccherà (tentando di accedere alla memoria non accessibile):

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

Devo aspettare fino a quando non viene caricato un UIWebView prima di eseguire il mio metodo, sono stato in grado di farlo funzionare eseguendo i controlli pronti UIWebView sul thread principale utilizzando GCD in combinazione con i metodi semaforo menzionati in questo thread. Il codice finale è simile al seguente:

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

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.