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

180

Estoy probando un código que realiza un procesamiento asincrónico utilizando 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 que parece un poco crudo, ¿conoces una mejor manera? Podría exponer la cola y luego bloquear llamando dispatch_sync:

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

... pero eso es tal vez exponer demasiado en el object.

zoul
fuente

Respuestas:

302

Tratando de usar a dispatch_semaphore. Debería verse más o menos así:

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

Esto debería comportarse correctamente incluso si runSomeLongOperationAndDo:decide que la operación no es lo suficientemente larga como para merecer un subproceso y se ejecuta sincrónicamente.

kperryua
fuente
61
Este código no me funcionó. Mi STAssert nunca se ejecutará. Tuve que reemplazar el 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
¿Eso es probablemente porque su bloque de finalización se envía a la cola principal? La cola se bloquea esperando el semáforo y, por lo tanto, nunca ejecuta el bloqueo. Vea esta pregunta sobre despachar en la cola principal sin bloquear.
zoul
3
Seguí la sugerencia de @Zoul & nicktmro. Pero parece que va a estar en un punto muerto. Caso de prueba '- [BlockTestTest testAsync]' comenzó. pero nunca terminó
NSCry
3
¿Necesitas liberar el semáforo bajo ARC?
Peter Warbo
14
Esto era exactamente lo que estaba buscando. ¡Gracias! @PeterWarbo no, no lo haces. El uso de ARC elimina la necesidad de hacer un dispatch_release ()
Hulvej
29

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 al probar el código asincrónico. 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 futuros lectores, si bien la técnica de despacho de semáforos es una técnica maravillosa cuando es absolutamente necesaria, debo confesar que veo demasiados desarrolladores nuevos, no familiarizados con buenos patrones de programación asincrónica, gravitan demasiado rápido a los semáforos como un mecanismo general para hacer asíncrono Las rutinas se comportan sincrónicamente. Peor aún, he visto a muchos de ellos usar esta técnica de semáforo desde 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 termine hasta que se realice la llamada asincrónica). Esta es una de esas situaciones raras en las que podría ser necesaria la técnica de semáforo para bloquear el hilo principal.

Entonces, con mis disculpas al autor de esta pregunta original, para quien la técnica del semáforo es sólida, escribo esta advertencia a todos los nuevos desarrolladores que ven esta técnica del semáforo y consideran aplicarla en su código como un enfoque general para tratar con asincrónico métodos: Tenga en cuenta que nueve de cada diez veces, la técnica del semáforo no esEl mejor enfoque al encontrar operaciones asincrónicas. En su lugar, familiarícese con los patrones de cierre / bloqueo de finalización, así como con los patrones y notificaciones de protocolo delegado. 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 sincrónica. Por lo general, hay buenas razones por las que las tareas asincrónicas se diseñaron para que se comporten de forma asincrónica, por lo que debe utilizar el patrón asincrónico correcto en lugar de intentar que se comporten de forma sincrónica.

Robar
fuente
1
Creo que esta debería ser la respuesta aceptada ahora. Aquí también están los documentos: developer.apple.com/library/prerelease/ios/documentation/…
hris.to
Tengo una pregunta sobre esto. Tengo un código asincrónico que realiza aproximadamente una docena de llamadas de descarga de AFNetworking para descargar un solo documento. Me gustaría programar descargas en un NSOperationQueue. A menos que use algo así como un semáforo, las descargas de documentos NSOperationaparecerán de inmediato y no habrá una cola real de descargas, sino que procederán simultáneamente, lo que no quiero. ¿Son razonables los semáforos aquí? ¿O hay una mejor manera de hacer que NSOperations espere el final asincrónico de los demás? ¿O algo mas?
Benjohn
No, no use semáforos en esta situación. Si tiene una cola de operaciones a la que está agregando los AFHTTPRequestOperationobjetos, debe crear una operación de finalización (que dependerá de las otras operaciones). O use grupos de despacho. Por cierto, dices que no quieres que se ejecuten simultáneamente, lo cual está bien si eso es lo que necesitas, pero pagas una seria penalización de rendimiento haciendo esto secuencialmente en lugar de concurrentemente. Generalmente uso maxConcurrentOperationCountde 4 o 5.
Rob
28

Recientemente volví a este problema 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 asincrónica con una devolución de llamada en una sincrónica en las pruebas:

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

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

Estos enfoques son incompatibles con la forma en que GCD está destinado a funcionar y terminarán causando puntos muertos y / o agotando la batería mediante un sondeo sin parar.

En otras palabras, reorganice su código para que no haya una espera síncrona de un resultado, sino que trate con un resultado que se le notifique sobre un cambio de estado (por ejemplo, devoluciones de llamada / protocolos delegados, estar disponible, desaparecer, errores, etc.). (Estos se pueden refactorizar en bloques si no te gusta el infierno de devolución de llamada). Porque así es como exponer el comportamiento real al resto de la aplicación que esconderlo 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 muckear con devoluciones de llamadas delegadas, envuélvalas en una clase proxy concreta que implemente el protocolo personalizado y guarde los diversos bloques en propiedades. Probablemente también proporcione constructores convenientes también.

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

(No pidas un ejemplo, porque es trivial y también tuvimos que invertir el tiempo para aprender los conceptos básicos del objetivo c).


fuente
1
Es una advertencia importante debido a los patrones de diseño de obj-C y a la
capacidad de prueba
8

Aquí hay un ingenioso truco 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 debe hacer es esperar usando dispatch_syncun bloque vacío para esperar sincrónicamente en una cola de despacho en serie hasta que se complete el bloque A-Synchronous.

Leslie Godwin
fuente
El problema con esta respuesta es que no aborda el problema original del OP, que es que la API que necesita ser utilizada toma un completeHandler como argumento y regresa de inmediato. Llamar a esa API dentro del bloque asíncrono de esta respuesta volvería de inmediato a pesar de que la finalizaciónHandler aún no se había ejecutado. Entonces el bloque de sincronización se estaría ejecutando antes de la finalización Handler.
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);
}

Ejemplo de uso:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];
Oliver Atkinson
fuente
2

También hay 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 objc.io para obtener más detalles). Y desde Xcode 6 hay una AsynchronousTestingcategoría XCTestque le permite escribir código como este:

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

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);
Peter DeWeese
fuente
1
Hay un error en el código anterior. De la NSCondition documentación de -waitUntilDate:"Debe bloquear el receptor antes de llamar a este método". Entonces el -unlockdebería ser después -waitUntilDate:.
Patrick
Esto no escala a nada que use múltiples hilos o colas de ejecución.
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]];
}

Esto lo hizo por mi.


fuente
3
así, hace que el uso intensivo de la CPU, aunque
Kevin
44
@kevin Sí, estas son las encuestas de guetos que matarán la batería.
@Barry, ¿cómo consume más batería? Por favor guía.
pkc456
@ pkc456 Eche un vistazo en un libro de ciencias de la computación sobre las diferencias entre el funcionamiento de las encuestas y la notificación asincrónica. Buena suerte.
2
Cuatro años y medio después y con el conocimiento y la experiencia que he adquirido, no recomendaría mi respuesta.
0

A veces, los bucles de tiempo de espera también son útiles. ¿Puede esperar hasta obtener alguna señal (puede ser BOOL) del método de devolución de llamada asincrónica, pero qué pasa si no hay respuesta alguna y desea salir de ese bucle? Aquí debajo hay una solución, mayormente respondida anteriormente, pero con una adición de 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];
}
Khulja Sim Sim
fuente
1
Mismo problema: falla de la vida útil de la batería.
1
@Barry No estoy seguro aunque haya mirado el código. Hay un período TIMEOUT_SECONDS dentro del cual si la llamada asincrónica no responde, se interrumpirá el ciclo. Ese es el truco para romper el punto muerto. Este código funciona perfectamente sin matar la batería.
Khulja Sim Sim
0

Solución muy primitiva al problema:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

Swift 4:

Úselo en synchronousRemoteObjectProxyWithErrorHandlerlugar de remoteObjectProxyal crear el objeto remoto. No más necesidad de un semáforo.

El siguiente ejemplo devolverá la versión recibida del proxy. Sin el synchronousRemoteObjectProxyWithErrorHandlerse bloqueará (intentando acceder a memoria no accesible):

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
}
Freek Sanders
fuente
-1

Tengo que esperar hasta que se cargue un UIWebView antes de ejecutar mi método, pude hacer que esto funcione realizando comprobaciones de UIWebView ready en el hilo principal usando GCD en combinación con los métodos de semáforo mencionados en este hilo. El código final se ve así:

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

}
Albert Renshaw
fuente