7

Me he estado preguntando, ¿puedes usar cancel/cancelAllOperations/.isCancelled con un hilo que has iniciado con GCD?¿Se puede usar cancel/isCancelled con GCD/dispatch_async?

Actualmente, solo uso un booleano como indicador, para cancelar el proceso en segundo plano.

Digamos que desea hacer un montón de procesamiento en segundo plano, mientras mantiene la IU receptiva para que pueda capturar un botón cancelar (o animar algo para mostrar que el procesador está funcionando). Así es como lo hacemos ...

@interface AstoundingView : UIView 
    { 
    BOOL pleaseAbandonYourEfforts; 
    blah 
    } 
@implementation AstoundingView 
// 
// these are the foreground routines... 
// begin, abandon and all-done 
// 
-(void)userHasClickedToBuildASpaceship 
    { 
    [YourUIStateMachine buildShip]; 
    [self procedurallyBuildEnormousSpaceship]; 
    } 
-(void)userHasClickedToAbandonBuildingTheSpaceship 
    { 
    [YourUIStateMachine inbetween]; 
    pleaseAbandonYourEfforts = false; // that's it! 
    } 
-(void)attentionBGIsAllDone 
    { 
// you get here when the process finishes, whether by completion 
// or if we have asked it to cancel itself. 
    [self typically setNeedsDisplay, etc]; 
    [YourUIStateMachine nothinghappening]; 
    } 
// 
// these are the background routines... 
// the kickoff, the wrapper, and the guts 
// 
// The wrapper MUST contain a "we've finished" message to home 
// The guts can contain messages to home (eg, progress messages) 
// 
-(void)procedurallyBuildEnormousSpaceship 
    { 
    // user has clicked button to build new spaceship 
    pleaseAbandonYourEfforts = FALSE; 
    dispatch_async(
     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), 
     ^{ [self actuallyProcedurallyBuildInBackground]; } 
     ); 

    // as an aside, it's worth noting that this does not work if you 
    // use the main Q rather than a global Q as shown. 
    // Thus, this would not work: 
    // dispatch_async(dispatch_get_main_queue(), ^{ ...; }); 
    } 

-(void)actuallyProcedurallyBuildInBackground 
    { 
    // we are actually in the BG here... 

    [self setUpHere]; 

    // set up any variables, contexts etc you need right here 
    // DO NOT open any variables, contexts etc in "buildGuts" 

    // when you return back here after buildGuts, CLEAN UP those 
    // variables, contexts etc at this level. 

    // (using this system, you can nest as deep as you want, and the 
    // one CHECKER pseudocall will always take you right out. 
    // You can insert CHECKERs anywhere you want.) 

    [self buildGuts]; 

    // Note that any time 'CHECKER' "goes off', you must fall- 
    // through to exactly here. This is the common fall-through point. 
    // So we must now tidy-up, to match setUpHere. 

    [self wrapUpHere]; 

    // when you get to here, we have finished (or, the user has cancelled 
    // the background operation) 

    // Whatever technique you use, 
    // MAKE SURE you clean up all your variables/contexts/etc before 
    // abandoning the BG process. 

    // and then you must do this...... 

    // we have finished. it's critical to let the foreground know NOW, 
    // or else it will sit there for about 4 to 6 seconds (on 4.3/4.2) 
    // doing nothing until it realises you are done 

    dispatch_sync(
     dispatch_get_main_queue(), 
     ^{[self attentionBGIsAllDone];} // would setneedsdisplay, etc 
     ); 

    return; 
    } 
-(void)buildGuts 
    { 
    // we are actually in the BG here... 

    // Don't open any local variables in here. 

    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 

    // to get stuff done from time to time on the UI, something like... 

    CHECKER 
    dispatch_sync(
     dispatch_get_main_queue(), 
     ^{[supportStuff pleasePostMidwayImage: 
      [UIImage imageWithCGImage:halfOfShip] ];} 
     ); 

    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 
    for (i = 1 to 10^9) 
     { 
     CHECKER 
     [self blah blah]; 
     } 
    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 

    return; 
    } 

y INSPECTOR no hace más que comprobar que la bandera es cierto ...

#define CHECKER if (pleaseAbandonYourEfforts == YES) \ 
{NSLog(@"Amazing Interruption System Working!");return;} 

Todo esto funciona a la perfección.

Pero ........ ¿es posible usar cancelar/cancelar todas las operaciones/.isCancelado con este tipo de uso de GCD?

¿Cuál es la historia aquí? Aclamaciones.


PD - para cualquier principiante que utilice esta plantilla de fondo de "seis partes".

Tenga en cuenta que como BJ destaca a continuación, cada vez que sales del proceso de BG ...

debe limpiar cualquier variable que tiene abiertas!

En mi idioma, debe asignar todas las variables, contextos, memoria, etc., específicamente en "setUpHere". Y debe liberarlos en "wrapUpHere". (Este idioma sigue funcionando si profundizas más y más, mientras estás en el BG.)

O bien, haz exactamente lo que BJ muestra en su ejemplo. (Si usa el método de BJ, tenga cuidado si profundiza)

Cualquiera sea el método que utilice, debe limpiar cualquier variable/contexto/memoria que tenga abierta, cuando salga del proceso de BG. Espero que ayude a alguien, alguna vez!

+0

Creo que no entendió mi punto acerca del punto de retorno único. Si asigna objetos temporales en el método 'buildGuts', debe asegurarse de liberarlos en ese método, antes de salir del método y perder el puntero a esos objetos. –

+1

Agregué otra actualización que aclaraba más mi declaración acerca del punto de retorno único. Tu '[autocompletar rápidamente en una reverencia]' llamada * no puede * manejar toda la administración de memoria que pueda ser necesaria, y simplemente asegurarte de que se ejecutará cierto método después de que el método 'buildGuts' no lo arregle. Cuando me refiero a un único punto de retorno, quise decir que solo hay un lugar donde el método puede salir. Bajo su implementación, puede salir a cualquier parte donde se use la macro 'CHECKER'. Ese no es un solo punto de retorno. –

+0

Hola BJ, agregue algunos comentarios explicando solo eso en el ejemplo de código, ¡salud! – Fattie

Respuesta

18

GCD no tiene soporte incorporado para la cancelación; si es importante poder cancelar su tarea en segundo plano, entonces, verificar una bandera como la que ha demostrado es una solución aceptable. Sin embargo, es posible que desee evaluar qué tan rápido debe responder la cancelación; Si algunas de esas llamadas a métodos son bastante cortas, es posible que pueda salirse con la suya con menos frecuencia.

Ha preguntado si puede usar los indicadores de NSOperation para admitir la cancelación. La respuesta es no. GCD no se basa en NSOperation. De hecho, en Snow Leopard NSOperation y NSOperationQueue se volvieron a implementar para usar GCD internamente. Entonces la dependencia es al revés. NSOperation es una construcción de nivel superior a GCD. Incluso si tuviera que usar NSOperation, su implementación de la cancelación sería en gran parte la misma; aún tendrá que marcar self.isCancelled periódicamente para ver si debe abandonar la construcción del barco espacial.

La única preocupación que tengo con su implementación de la macro CHECKER es que implementa un return inesperado. Como tal, debes tener cuidado con las pérdidas de memoria. Si ha configurado su propio NSAutoreleasePool en el hilo de fondo, necesita drain antes de volver.Si tiene alloc ed o retain ed cualquier objeto, puede necesitar release antes de devolverlo.

Dado que toda la limpieza debe realizarse en cada control, le recomendamos que se mueva hacia un único punto de retorno. Una forma de hacer esto sería ajustar cada una de sus llamadas a métodos en un bloque if (pleaseAbandonYourEfforts == NO) { }. Esto le permitiría llegar rápidamente al final del método una vez que se solicitó la cancelación, y mantener su limpieza en un solo lugar. Otra opción, aunque a algunos no les guste, sería hacer que la macro use la llamada goto cleanup; y defina una etiqueta cleanup: cerca del final del método donde se libera todo lo que necesita ser liberado. Algunas personas no les gusta usar goto de una manera casi religiosa, pero he descubierto que un salto hacia delante a una etiqueta de limpieza como esta a menudo es una solución más limpia que las alternativas. Si no te gusta, envolver todo en un bloque if funciona igual de bien.


Editar

me siento la necesidad de aclarar aún más mi declaración anterior acerca de tener un solo punto de retorno. Con la macro CHECKER como se definió anteriormente, el método -buildGuts puede regresar en cualquier punto donde se usa esa macro. Si hay objetos retenidos locales para ese método, se deben limpiar antes de volver. Por ejemplo, imagina esta modificación muy razonable a su -buildGuts método:

-(void)buildGuts 
{ 
    // we are actually in the BG here... 

    NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 

    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self recordSerialNumberUsingFormatter:formatter]; 

    // ... etc ... 

    [formatter release]; 

    return; 
} 

Tenga en cuenta que en este caso, si el CHECKER macro nos hace volver antes del final del método, el objeto de formatter no será lanzado y se filtró. Si bien la llamada [self quickly wrap up in a bow] puede gestionar la limpieza de cualquier objeto accesible a través de una variable de instancia o mediante un puntero global, no puede liberar objetos que solo estaban disponibles localmente en el método buildGuts. Esto es por lo que sugiere la implementación goto cleanup, lo que se vería así:

#define CHECKER if (pleaseAbandonYourEfforts == YES) { goto cleanup; } 

-(void)buildGuts 
{ 
    // we are actually in the BG here... 

    NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 

    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self blah blah]; 
    CHECKER 
    [self recordSerialNumberUsingFormatter:formatter]; 

    // ... etc ... 

cleanup: 
    [formatter release]; 

    return; 
} 

En esta implementación, formatter siempre se dará a conocer, independientemente del momento en que ocurre la cancelación.

En resumen, cada vez que maneja una macro que puede hacer que regrese de un método, necesita estar muy seguro de que antes de que regrese prematuramente, toda la administración de la memoria se ha solucionado. Es difícil hacerlo limpiamente con una macro que provoca un retorno.

+2

hola BJ - gracias por su magnífica respuesta explicando GCD y su relación con NSOperation/etc. ¡¡Fantástico!! Así que, de nuevo, como dices ** GCD no tiene compatibilidad incorporada para la cancelación **. Si ese es el caso, y no hay un idioma real, entonces lo mejor es hacerlo uno mismo, supongo. Excelente gracias. – Fattie

+0

Hola BJ .. con respecto a la limpieza de todas las variables locales cuando rompes hacia arriba: con seguridad. Agregué más comentarios dentro del código para explicar mi modismo. – Fattie

1

¡Gracias por la discusión! En mi caso, quería permitir la emisión de una nueva solicitud asíncrona que cancelaría la anterior si aún no se hubiera completado. Con el ejemplo anterior, tendría que esperar de algún modo una señal a través de la devolución de llamada attentionBGIsAllDone de que la solicitud pendiente se canceló antes de poder emitir una nueva solicitud. En lugar de ello, he creado un simple envoltorio booleano, una instancia de la que podía asociarse con una solicitud pendiente:

@interface MyMutableBool : NSObject { 
    BOOL value; 
} 
@property BOOL value; 
@end 

@implementation MyMutableBool 
@synthesize value; 
@end 

y utilizar una instancia de que por pleaseAbandonYourEfforts. Antes de hacer mi dispatch_async (es decir, en procedurallyBuildEnormousSpaceship arriba), me cancelar la solicitud de edad y prepararse para el nuevo como sigue:

// First cancel any old outstanding request. 
cancelRequest.value = YES; 
// Now create a new flag to signal whether or not to cancel the new request. 
MyMutableBool *cancelThisRequest = [[[MyMutableBool alloc] init] autorelease]; 
self.cancelRequest = cancelThisRequest; 

Mi bloque de realizar la tarea asíncrona tendría que comprobar cancelThisRequest.value por supuesto.

Cuestiones relacionadas