¿Cómo espero a que termine un bloque enviado asincrónicamente?

Estoy probando un código que procesa de manera asíncrona usando Grand Central Dispatch. El código de prueba se ve así:

[object runSomeLongOperationAndDo:^{ STAssert… }]; 

Las pruebas tienen que esperar a que termine la operación. Mi solución actual se ve así:

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

Lo cual se ve un poco crudo, ¿conoces una mejor manera? Podría exponer la cola y luego bloquear llamando a dispatch_sync :

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

… pero eso tal vez expone demasiado al object .

Intenta usar un dispatch_sempahore . Debería verse algo como esto:

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

Esto debería comportarse correctamente incluso si runSomeLongOperationAndDo: decide que la operación no es lo suficientemente larga como para merecer el enhebrado y se ejecuta de forma síncrona.

Además de la técnica de semáforo cubierta exhaustivamente en otras respuestas, ahora podemos usar XCTest en Xcode 6 para realizar pruebas asincrónicas a través de XCTestExpectation . Esto elimina la necesidad de semáforos cuando se prueba el código asíncrono. Por ejemplo:

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

Por el bien de los lectores futuros, aunque la técnica del envío de semáforos es una técnica maravillosa cuando es absolutamente necesaria, debo confesar que veo demasiados desarrolladores nuevos, que no están familiarizados con los buenos patrones de progtwigción asincrónica, gravitan demasiado rápido hacia los semáforos como mecanismo general para hacer asincrónicos las rutinas se comportan sincrónicamente Peor aún, he visto que muchos de ellos usan esta técnica de semáforo de la cola principal (y nunca debemos bloquear la cola principal en las aplicaciones de producción).

Sé que este no es el caso aquí (cuando se publicó esta pregunta, no había una buena herramienta como XCTestExpectation , también, en estas suites de prueba, debemos asegurarnos de que la prueba no finalice hasta que se realice la llamada asincrónica). Esta es una de esas situaciones raras donde la técnica de semáforo para bloquear el hilo principal podría ser necesaria.

Entonces, con mis disculpas al autor de esta pregunta original, para quien la técnica de semáforo es sólida, escribo esta advertencia a todos los nuevos desarrolladores que ven esta técnica de semáforo y considero aplicarla en su código como un enfoque general para tratar con asincrónicos Métodos: tenga en cuenta que nueve veces de cada diez, la técnica de semáforo no es el mejor enfoque cuando se contabilizan las operaciones asincrónicas. En su lugar, familiarícese con los patrones de bloque / cierre de finalización, así como con los patrones y notificaciones de protocolos delegates. A menudo, estas son formas mucho mejores de tratar con tareas asincrónicas, en lugar de utilizar semáforos para que se comporten de forma sincronizada. Por lo general, hay buenas razones para que las tareas asincrónicas se diseñen para que se comporten de forma asíncrona, por lo tanto, utilice el patrón asíncrono correcto en lugar de intentar que se comporten de forma sincronizada.

Recientemente volví a este tema y escribí la siguiente categoría en 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 

De esta manera, puedo convertir fácilmente una llamada asíncrona con una callback en una síncrona en las pruebas:

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

En general, no use ninguna de estas respuestas, a menudo no se escalarán (hay excepciones aquí y allá, seguro)

Estos enfoques son incompatibles con la forma en que se pretende que GCD funcione y terminará causando interlockings y / o matando la batería mediante sondeos continuos.

En otras palabras, reordene su código para que no haya una espera sincrónica para un resultado, sino que se ocupe de un resultado al que se le notifica el cambio de estado (por ejemplo, devoluciones de llamada / protocolos delegates, disponibilidad, desaparición, errores, etc.). (Estos pueden refactorizarse en bloques si no te gusta el infierno de callback). Porque así es como exponer el comportamiento real al rest de la aplicación que ocultarlo detrás de una fachada falsa.

En su lugar, use NSNotificationCenter , defina un protocolo de delegado personalizado con devoluciones de llamada para su clase. Y si no le gusta el destripado con callbacks de delegado por todas partes, envuélvalas en una clase de proxy concreta que implemente el protocolo personalizado y guarde las diversas propiedades de bloque. Probablemente también proporcione constructores de conveniencia también.

El trabajo inicial es un poco más, pero reducirá la cantidad de terribles condiciones de carrera y las encuestas de asesinatos a batería a largo plazo.

(No pida un ejemplo, porque es trivial y tuvimos que invertir el tiempo para aprender los principios básicos de Objective-C también).

Aquí hay un truco ingenioso que no usa un semáforo:

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

Lo que hace es esperar usando dispatch_sync con un bloque vacío para esperar sincrónicamente en una cola de despacho en serie hasta que el bloque A-Sincrónico haya finalizado.

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

Ejemplo de uso:

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

También está SenTestingKitAsync que te permite escribir código como este:

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

(Consulte el artículo de objc.io para obtener más información.) Y desde Xcode 6 hay una categoría de XCTest AsynchronousTesting en XCTest que le permite escribir un código como este:

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

Aquí hay una alternativa de una de mis pruebas:

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

Esto lo hizo por mí.

A veces, los bucles Timeout también son útiles. ¿Puedes esperar hasta obtener alguna señal (puede ser BOOL) del método de callback asíncrona, pero qué pasa si no hay respuesta alguna vez, y quieres salir de ese ciclo? A continuación, se muestra una solución, en su mayoría respondida anteriormente, pero con una adición de tiempo de espera.

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

Una solución muy primitiva al problema:

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