Domanda

sto testando un codice che si occupa di elaborazione asincrona utilizzando Grand Central Dispatch. L'aspetto del codice di test come questo:

[object runSomeLongOperationAndDo:^{
    STAssert…
}];

I test devono attendere per l'operazione alla fine. Il mio attuale aspetto soluzione di questo tipo:

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

Il che sembra un po 'grezza, sai un modo migliore? Potrei esporre la coda e poi bloccare chiamando dispatch_sync:

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

... ma questa è forse esporre troppo sul object.

È stato utile?

Soluzione

Provare con un dispatch_sempahore. Esso dovrebbe essere simile a questo:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert…

    dispatch_semaphore_signal(sema);
}];

dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);

Questo dovrebbe comportarsi correttamente anche se runSomeLongOperationAndDo: decide che l'operazione non è in realtà abbastanza a lungo per il merito threading e gestisce in modo sincrono, invece.

Altri suggerimenti

Oltre alla tecnica semaforo coperto esaurientemente in altre risposte, possiamo ora utilizzare XCTest in Xcode 6 per eseguire test asincroni via XCTestExpectation. Questo elimina la necessità di semafori durante il test codice asincrono. Ad 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 motivi di futuri lettori, mentre la tecnica l'invio semaforo è una tecnica meravigliosa quando è assolutamente necessario, devo confessare che io vedo troppi nuovi sviluppatori, non hanno familiarità con i buoni modelli di programmazione asincrona, gravitano troppo velocemente per i semafori come un meccanismo generale per fare le routine asincrone si comportano in modo sincrono. Peggio Ho visto molti di loro usano questa tecnica semaforo dalla coda principale (e non dobbiamo mai bloccare la coda principale di applicazioni di produzione).

So che questo non è il caso qui (quando questa domanda è stata pubblicata, non era uno strumento bello come XCTestExpectation, anche, in queste suite di test, dobbiamo garantire il test non termina fino a quando la chiamata asincrona è fatto ). Questa è una di quelle rare situazioni in cui potrebbe essere necessario la tecnica del semaforo per bloccare il filo principale.

Quindi, con le mie scuse a l'autore di questo domanda iniziale, per i quali la tecnica semaforo è suono, scrivo questo avvertimento a tutti quei nuovi sviluppatori che vedono questa tecnica semaforo e considerano la sua applicazione nel loro codice come approccio generale per si tratta di metodi asincroni: sappiate che nove volte su dieci, la tecnica semaforo è non l'approccio migliore quando encounting operazioni asincrone. Invece, familiarizzare con blocco di completamento / modelli di chiusura, così come i modelli delegato-protocollo e le notifiche. Si tratta spesso di molto migliori modi di affrontare compiti asincroni, invece di usare i semafori per farli comportarsi in modo sincrono. Di solito ci sono buone ragioni che le attività asincrone sono stati progettati per comportarsi in modo asincrono, in modo da utilizzare il modello asincrono destra piuttosto che cercare di farli comportarsi in modo sincrono.

Recentemente ho arrivati ??a questo problema di nuovo e ha scritto la seguente categoria sul 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 chiamata asincrona con un callback in uno sincrono nei test:

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

In generale non utilizzare nessuna di queste risposte, che spesso non sarà in scala (c'è eccezioni qua e là, certo)

Questi approcci sono incompatibili con il modo GCD ha lo scopo di lavoro e finirà per causare uno stallo e / o ucciso la batteria polling senza sosta.

In altre parole, riorganizzare il codice in modo che non v'è nessuna attesa sincrona per un risultato, ma invece che fare con il risultato di essere informata di cambiamento di stato (ad esempio, le richiamate / protocolli delegato, essendo disponibile, andando via, errori, etc. ). (Questi possono essere riscritta in blocchi, se non ti piace richiamata l'inferno.) Perché questo è il modo di esporre il comportamento reale per il resto della app di nascondere dietro una falsa facciata.

Al contrario, utilizzare NSNotificationCenter , definire un protocollo delegato personalizzato con callback per la classe. E se non ti piace pasticciare con i callback delegato tutto, avvolgerli in una classe proxy concreta che implementa il protocollo personalizzato e salva i vari blocchi in proprietà. Probabilmente anche fornire costruttori convenienza pure.

Il lavoro iniziale è leggermente più ma ridurrà il numero di terribili da corsa condizioni e batteria ucciso polling nel lungo periodo.

(non chiedere per un esempio, perché è banale e abbiamo dovuto investire il tempo di imparare Objective-C basi troppo.)

Ecco un trucco ingegnoso che non utilizza un semaforo:

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

Quello che fate è attesa utilizzando dispatch_sync con un blocco vuoto per Synchronously attesa in una coda di invio di serie fino a quando il blocco A-sincrono è stata completata.

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

C'è anche SenTestingKitAsync che permette di scrivere codice come questo:

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

(vedi objc.io articolo per i dettagli.) E poiché Xcode 6 c'è una categoria AsynchronousTesting su XCTest che 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];

Ecco un alternativa da 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);
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 ha fatto per me.

A volte, i cicli Timeout sono anche utili. Che tu possa aspettare fino ad arrivare qualche segnale (può essere BOOL) dal metodo asincrono di callback, ma che cosa se nessuna risposta mai, e si desidera uscire da quel ciclo? Qui di seguito è la soluzione, per lo più risposto sopra, ma con l'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];
}

Soluzione molto primitivo al problema:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

Swift 4:

Uso synchronousRemoteObjectProxyWithErrorHandler anziché remoteObjectProxy quando si crea l'oggetto remoto. Non è più necessario per un semaforo.

Di seguito esempio restituirà la versione ricevuta dal proxy. Senza il synchronousRemoteObjectProxyWithErrorHandler andrà in crash (cercando di accesso 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
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top