iphone multithreading ios nsoperation grand-central-dispatch

iphone - ¿Puedes usar cancel/isCancelled con GCD/dispatch_async?



multithreading ios (2)

Me he estado preguntando, ¿puedes usar cancel / cancelAllOperations / .isCancelled con un hilo que hayas lanzado con GCD?

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

Supongamos que desea hacer un montón de procesamiento en segundo plano, mientras mantiene la IU receptiva para que pueda capturar un botón de 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 CHECKER no hace nada más que verificar que la bandera sea verdadera ...

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

Todo esto funciona perfectamente.

Pero ........ ¿ es posible usar cancel / cancelAllOperations / .isCancelled con este tipo de uso de GCD?

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

PD: para cualquier principiante que use esta plantilla de fondo de "seis partes".

Tenga en cuenta que como BJ se destaca a continuación, cada vez que salga del proceso de bg ...

¡debes limpiar las variables que tienes abiertas!

En mi idioma, debes asignar todas las variables, contextos, memoria, etc., específicamente en "setUpHere". Y debe liberarlos en "wrapUpHere". (Este modismo continúa funcionando si vas más y más profundo, mientras estás en el BG).

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

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


GCD no tiene soporte integrado 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.

Me preguntó si podía 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 self.isCancelled verificarlo. Se self.isCancelled periódicamente para ver si debes 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, debe drain antes de volver. Si ha alloc o retain objetos, es posible que deba release antes de regresar.

Dado que toda la limpieza debe ocurrir en cada control, es posible que desee considerar avanzar hacia un único punto de retorno. Una forma de hacerlo sería envolver cada una de sus llamadas de método en un 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 call goto cleanup; y define una cleanup: etiqueta cerca del final del método donde liberas todo lo que necesita ser liberado. A algunas personas les disgusta 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

Siento la necesidad de aclarar aún más mi declaración anterior sobre 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 para tu método -buildGuts :

-(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 la macro CHECKER nos hace regresar antes del final del método, entonces el objeto en el formatter no se liberará y se filtrará . Mientras que 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 . Es por eso que sugerí la implementación de goto cleanup , 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; }

Bajo esta implementación, el formatter siempre se lanzará, independientemente de cuándo ocurra la cancelación.

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


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 alguna manera una señal a través de la devolución de llamada attentionBGIsAllDone que la solicitud pendiente se canceló antes de poder emitir una nueva solicitud. En cambio, creé un contenedor booleano simple, una instancia de la cual podría asociar con una solicitud pendiente:

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

Y use una instancia de eso para por pleaseAbandonYourEfforts . Antes de hacer mi dispatch_async (es decir, en procedurallyBuildEnormousSpaceship ), cancelo la solicitud anterior y me preparo para la nueva de la siguiente manera:

// 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 que realiza la tarea asíncrona debería marcar cancelThisRequest.value por supuesto.