2008-09-29 8 views
39

En la documentación de Apple para NSRunLoop, hay un código de muestra que demuestra la suspensión de la ejecución mientras se espera que una bandera sea configurada por otra cosa.¿La mejor manera de hacer que NSRunLoop espere a que se establezca una bandera?

BOOL shouldKeepRunning = YES;  // global 
NSRunLoop *theRL = [NSRunLoop currentRunLoop]; 
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); 

He estado usando este y funciona, pero en la investigación de un problema de rendimiento que lo rastreó a este pedazo de código. Uso casi exactamente el mismo fragmento de código (solo el nombre de la bandera es diferente :) y si pongo un NSLog en la línea después de que se está configurando la bandera (en otro método) y luego una línea después del while(), hay un aparentemente aleatorio espera entre las dos declaraciones de registro de varios segundos.

La demora no parece ser diferente en máquinas más lentas o más rápidas, pero varía de una ejecución a otra por al menos un par de segundos y hasta 10 segundos.

He solucionado este problema con el siguiente código, pero no parece correcto que el código original no funcione.

NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1]; 
while (webViewIsLoading && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil]) 
    loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1]; 

utilizando este código, las declaraciones de registro cuando el ajuste de la bandera y después del bucle, mientras que ahora son consistentemente menos de 0,1 segundos de diferencia.

¿Alguien alguna idea de por qué el código original muestra este comportamiento?

+0

Creo que entendió mal el ejemplo dado en la documentación; el uso de este código de ejemplo es cuando desea finalizar la ejecución de un runloop (no recibir notificaciones sobre el cambio de indicador) https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/ NSRunLoop_Class/Reference/Reference.html # // apple_ref/occ/instm/NSRunLoop/run – kernix

Respuesta

35

Runloops puede ser una especie de caja mágica donde las cosas simplemente suceden.

Básicamente le está diciendo al runloop que procese algunos eventos y luego regrese. O devuelve si no procesa ningún evento antes de que se agote el tiempo de espera.

Con 0.1 segundos de tiempo de espera, está esperando el tiempo de espera más de las veces. El runloop dispara, no procesa ningún evento y regresa en 0.1 de segundo. Ocasionalmente tendrá la oportunidad de procesar un evento.

Con su timeout distante, el runloop esperará hasta que se procese un evento. Entonces cuando vuelve a usted, acaba de procesar un evento de algún tipo.

Un valor de tiempo de espera corto consumirá considerablemente más CPU que el tiempo de espera infinito, pero hay buenas razones para usar un tiempo de espera corto, por ejemplo, si desea terminar el proceso/thread en el que se ejecuta runloop. Probablemente quiera el runloop para notar que una bandera ha cambiado y que necesita rescatar CUANTO ANTES.

Es posible que desee jugar con los observadores runloop para que pueda ver exactamente lo que está haciendo el runloop.

Consulte this Apple doc para obtener más información.

2

Tuve problemas similares al intentar administrar NSRunLoops. El discussion para runMode:beforeDate: en la página de referencias de clase dice:

Si no hay fuentes de entrada o temporizadores están unidos al bucle de ejecución, este método se cierra inmediatamente; de lo contrario, vuelve después de que se procesa la primera fuente de entrada o se alcanza el límite Fecha límite. La eliminación manual de todas las fuentes de entrada y temporizadores conocidos del bucle de ejecución no garantiza que el bucle de ejecución salga. Mac OS X puede instalar y eliminar fuentes de entrada adicionales según sea necesario para procesar las solicitudes dirigidas a la cadena del receptor. Esas fuentes, por lo tanto, podrían evitar que el ciclo de ejecución salga.

Mi mejor conjetura es que una fuente de entrada está conectado a su NSRunLoop, tal vez por el sistema operativo en sí X, y que runMode:beforeDate: está bloqueando hasta que la fuente de entrada o bien ha procesado alguna entrada, o está eliminado. En su caso tomaría "un par de segundos y hasta 10 segundos" para que esto suceda, en cuyo punto runMode:beforeDate: volvería con un booleano, el while() se ejecutaría nuevamente, detectaría que shouldKeepRunning se configuró en NO, y el ciclo terminaría.

Con su refinamiento, el runMode:beforeDate: volverá en 0,1 segundos, independientemente de si tiene o no fuentes de entrada conectadas o si ha procesado alguna entrada. Es una suposición educada (no soy un experto en el funcionamiento interno del ciclo de carrera), pero creo que su refinamiento es la manera correcta de manejar la situación.

10

Si desea poder establecer su variable de indicador y hacer que el ciclo de ejecución se note inmediatamente, simplemente use -[NSRunLoop performSelector:target:argument:order:modes: para solicitar al ciclo de ejecución que invoque el método que establece el indicador en falso. Esto hará que su ciclo de ejecución gire inmediatamente, se invoque el método y luego se verificará el indicador.

+1

Gracias Chris, desafortunadamente esto no funcionará ya que la bandera se está configurando en un método de delegado en WebView, así que no puedo simplemente llamar de inmediato. –

5

En su código, el hilo actual verificará que la variable haya cambiado cada 0.1 segundos. En el ejemplo del código de Apple, cambiar la variable no tendrá ningún efecto. El runloop se ejecutará hasta que procese algún evento. Si el valor de webViewIsLoading ha cambiado, no se genera ningún evento automáticamente, por lo que permanecerá en el bucle, ¿por qué se rompería? Permanecerá allí, hasta que obtenga algún otro evento para procesar, luego saldrá de él. Esto puede suceder en 1, 3, 5, 10 o incluso 20 segundos. Y hasta que eso suceda, no saldrá del runloop y por lo tanto no notará que esta variable ha cambiado. IOW el código de Apple que citó es indeterminista. Este ejemplo solo funcionará si el cambio de valor de webViewIsLoading también crea un evento que causa que el runloop se active y este no parece ser el caso (o al menos no siempre).

Creo que deberías volver a pensar el problema. Dado que su variable se llama webViewIsLoading, ¿espera a que se cargue una página web? ¿Estás usando Webkit para eso? Dudo que necesites tal variable en absoluto, ni ninguno de los códigos que has publicado. En su lugar, debe codificar su aplicación de forma asincrónica. Debe iniciar el "proceso de carga de la página web" y luego volver al ciclo principal y tan pronto como la página termine de cargarse, debe publicar de forma asíncrona una notificación que se procesa dentro del hilo principal y ejecuta el código que debe ejecutarse tan pronto como la carga ha terminado.

+1

Esta es una gran explicación de por qué RunLoop no estaba explotando, gracias. Hay una buena razón por la cual la carga de WebView debe ser modal y debo mantenerla así. –

15

bien, te expliqué el problema, esto es una solución posible:

@implementation MyWindowController 

volatile BOOL pageStillLoading; 

- (void) runInBackground:(id)arg 
{ 
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 

    // Simmulate web page loading 
    sleep(5); 

    // This will not wake up the runloop on main thread! 
    pageStillLoading = NO; 

    // Wake up the main thread from the runloop 
    [self performSelectorOnMainThread:@selector(wakeUpMainThreadRunloop:) withObject:nil waitUntilDone:NO]; 

    [pool release]; 
} 


- (void) wakeUpMainThreadRunloop:(id)arg 
{ 
    // This method is executed on main thread! 
    // It doesn't need to do anything actually, just having it run will 
    // make sure the main thread stops running the runloop 
} 


- (IBAction)start:(id)sender 
{ 
    pageStillLoading = YES; 
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil]; 
    [progress setHidden:NO]; 
    while (pageStillLoading) { 
     [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 
    } 
    [progress setHidden:YES]; 
} 

@end 

inicio muestra un indicador de progreso y captura el hilo principal en una runloop interna. Permanecerá allí hasta que el otro hilo anuncie que está hecho. Para activar el hilo principal, se procesará una función sin otro propósito que levantar el hilo principal.

Esta es solo una forma de cómo hacerlo. Una notificación publicada y procesada en el hilo principal podría ser preferible (también podrían registrarse otros hilos), pero la solución anterior es la más simple que se me ocurre. Por cierto, no es realmente seguro para subprocesos. Para que realmente sea seguro para subprocesos, cada acceso al booleano debe ser bloqueado por un objeto NSLock desde cualquier subproceso (usar dicho bloqueo también hace obsoleto a "volátil", ya que las variables protegidas por un bloqueo son implícitamente volátiles según el estándar POSIX; El estándar C, sin embargo, no sabe sobre bloqueos, por lo que aquí solo volátil puede garantizar que este código funcione; GCC no necesita que se establezca el volátil para una variable protegida por bloqueos.

+0

Muy bueno. Esto agrega una nueva herramienta a mi objc arsenal. Considere agregar su técnica a [este hilo] [1] [1]: http://stackoverflow.com/questions/155964/what-are-best-practices-that-you-use-when-writing-objective- c-y-cacao # 156343 – schwa

+0

Gran técnica, muy elegante. –

+0

Quiero saber si el UIButton (emisor) mantendrá el estado UIControlStateSelected durante 5 segundos? – user501836

11

En general, si está procesando eventos usted mismo en un bucle, lo está haciendo mal. Puede causar una tonelada de problemas desordenados, en mi experiencia.

Si desea ejecutar modalmente, por ejemplo, mostrar un panel de progreso, ejecute de manera modal. Siga adelante y use los métodos NSApplication, ejecute modally para la hoja de progreso, luego detenga el modal cuando se complete la carga. Consulte la documentación de Apple, por ejemplo http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html.

Si solo desea que la vista esté activa durante la carga, pero no desea que sea modal (por ejemplo, si desea que otras vistas puedan responder a eventos), entonces debe hacer algo mucho más simple. Por ejemplo, puede hacer esto:

- (IBAction)start:(id)sender 
{ 
    pageStillLoading = YES; 
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil]; 
    [progress setHidden:NO]; 
} 

- (void)wakeUpMainThreadRunloop:(id)arg 
{ 
    [progress setHidden:YES]; 
} 

Y listo. ¡No es necesario mantener el control del ciclo de ejecución!

-Wil

0

Su segundo ejemplo sólo el trabajo en torno a medida que electoral que controle la entrada del bucle de ejecución en el intervalo de tiempo 0.1.

De vez en cuando me encuentro con una solución para su primer ejemplo:

BOOL shouldKeepRunning = YES;  // global 
NSRunLoop *theRL = [NSRunLoop currentRunLoop]; 
while (shouldKeepRunning && [theRL runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]); 
Cuestiones relacionadas