Le meilleur moyen de faire en sorte que NSRunLoop attende qu’un drapeau soit défini?

Dans la documentation Apple pour NSRunLoop, il existe un exemple de code démontrant l’exécution de la suspension en attendant qu’un autre paramètre soit défini pour un indicateur.

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

Je l’ai utilisé et cela fonctionne, mais en enquêtant sur un problème de performance, je l’ai suivi jusqu’à ce morceau de code. J’utilise presque exactement le même morceau de code (juste le nom du drapeau est différent 🙂 et si je mets un NSLog sur la ligne après le réglage de l’indicateur (dans une autre méthode), puis une ligne après le while() est une attente apparemment aléatoire entre les deux instructions de journal de plusieurs secondes.

Le délai ne semble pas être différent sur les machines plus lentes ou plus rapides, mais varie d’une exécution à l’autre au moins quelques secondes et jusqu’à 10 secondes.

J’ai contourné ce problème avec le code suivant, mais il ne semble pas correct que le code d’origine ne fonctionne pas.

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

En utilisant ce code, les instructions du journal lors de la définition de l’indicateur et après la boucle while sont désormais séparées de moins de 0,1 seconde.

Quelqu’un at-il des idées pourquoi le code d’origine présente ce comportement?

Les runloops peuvent être un peu une boîte magique où tout se passe.

Fondamentalement, vous dites au runloop d’aller traiter certains événements puis de revenir. OU retournez s’il ne traite aucun événement avant que le délai d’attente ne soit atteint.

Avec un délai d’attente de 0,1 seconde, vous attendez le délai d’attente plus souvent qu’autrement. Le runloop se déclenche, ne traite aucun événement et retourne en 0.1 de seconde. Parfois, il aura l’occasion de traiter un événement.

Avec votre délai d’attente distant, le runloop attendra jusqu’à ce qu’il traite un événement. Donc, quand il vous revient, il vient de traiter un événement quelconque.

Une courte valeur de timeout consumra beaucoup plus de CPU que le délai d’infini, mais il y a de bonnes raisons d’utiliser un court délai, par exemple si vous souhaitez mettre fin au processus / thread dans lequel s’exécute runloop. qu’un drapeau a changé et qu’il doit renflouer au plus vite.

Vous voudrez peut-être jouer avec les observateurs de runloop pour voir exactement ce que fait le runloop.

Voir ce document Apple pour plus d’informations.

Ok, je vous ai expliqué le problème, voici une solution possible:

 @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 

start affiche un indicateur de progression et capture le thread principal dans un runloop interne. Il restra là jusqu’à ce que l’autre thread annonce qu’il est fait. Pour réveiller le thread principal, il va traiter une fonction sans autre but que de réveiller le thread principal.

C’est juste un moyen de le faire. Une notification postée et traitée sur le thread principal peut être préférable (d’autres threads peuvent également s’enregistrer), mais la solution ci-dessus est la plus simple que je puisse imaginer. BTW ce n’est pas vraiment thread-safe. Pour être vraiment thread-safe, tout access au booléen doit être verrouillé par un object NSLock à partir de l’un ou l’autre thread (l’utilisation d’un tel verrou rend obsolète “volatile”, les variables protégées par un verrou étant implicitement volatiles). Le standard C ne connaît toutefois pas les verrous, donc seul le volatil peut garantir ce code pour qu’il fonctionne, GCC n’a pas besoin d’être volatile pour être défini pour une variable protégée par des verrous).

En général, si vous traitez vous-même des événements en boucle, vous ne le faites pas correctement. Cela peut causer une tonne de problèmes désordonnés, selon mon expérience.

Si vous souhaitez exécuter modalement – par exemple, en affichant un panneau de progression – exécutez modalement! Allez-y et utilisez les méthodes NSApplication, exécutez modalement la feuille de progression, puis arrêtez le modal lorsque le chargement est terminé. Voir la documentation Apple, par exemple http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html .

Si vous voulez juste une vue pour la durée de votre chargement, mais vous ne voulez pas qu’elle soit modale (par exemple, vous voulez que d’autres vues puissent répondre aux événements), alors vous devriez faire quelque chose de beaucoup plus simple. Par exemple, vous pouvez faire ceci:

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

Et tu as fini. Pas besoin de garder le contrôle de la boucle d’exécution!

-Wil

Si vous voulez pouvoir définir votre variable d’indicateur et que la boucle d’exécution soit immédiatement -[NSRunLoop performSelector:target:argument:order:modes: , utilisez simplement -[NSRunLoop performSelector:target:argument:order:modes: pour demander à la boucle d’exécution d’appeler la méthode qui définit l’indicateur sur false. Cela fera tourner votre boucle d’exécution immédiatement, la méthode à invoquer, puis le drapeau sera vérifié.

À votre code, le thread en cours vérifiera que la variable a changé toutes les 0,1 secondes. Dans l’exemple de code Apple, la modification de la variable n’aura aucun effet. Le runloop fonctionnera jusqu’à ce qu’il traite un événement. Si la valeur de webViewIsLoading a changé, aucun événement n’est généré automatiquement, il restra donc dans la boucle, pourquoi en sortirait-il? Il restra là jusqu’à ce qu’il y ait un autre événement à traiter, alors il en sortira. Cela peut se produire en 1, 3, 5, 10 ou même 20 secondes. Et jusqu’à ce que cela se produise, il ne sortira pas du runloop et ne remarquera donc pas que cette variable a changé. IOW le code Apple que vous avez cité est indéterminé. Cet exemple ne fonctionnera que si le changement de valeur de webViewIsLoading crée également un événement provoquant le réveil du runloop, ce qui semble ne pas être le cas (ou du moins pas toujours).

Je pense que vous devriez repenser le problème. Puisque votre variable s’appelle webViewIsLoading, attendez-vous qu’une page Web soit chargée? Utilisez-vous Webkit pour cela? Je doute que vous ayez besoin d’une telle variable, ni du code que vous avez publié. Au lieu de cela, vous devez coder votre application de manière asynchrone. Vous devez lancer le “processus de chargement de la page Web” puis revenir à la boucle principale et dès que la page est chargée, vous devez publier une notification traitée de manière asynchrone dans le thread principal et exécuter le code à exécuter dès que le chargement est terminé.

J’ai eu des problèmes similaires en essayant de gérer NSRunLoops . La discussion pour runMode:beforeDate: sur la page des références de classe dit:

Si aucune source d’entrée ou aucun temporisateur n’est associé à la boucle d’exécution, cette méthode se ferme immédiatement. sinon, il retourne une fois que la première source d’entrée est traitée ou que limitDate est atteint. Supprimer manuellement toutes les sources d’entrée et tous les timers connus de la boucle d’exécution ne garantit pas que la boucle d’exécution se fermera. Mac OS X peut installer et supprimer des sources d’entrée supplémentaires pour traiter les demandes ciblées sur le thread du destinataire. Ces sources pourraient donc empêcher la boucle d’exécution de sortir.

Ma meilleure supposition est qu’une source d’entrée est attachée à votre NSRunLoop , peut-être par OS X lui-même, et que runMode:beforeDate: bloque jusqu’à ce que cette source soit traitée ou soit supprimée. Dans votre cas, cela prenait ” quelques secondes et jusqu’à 10 secondes ” pour que cela se produise, point où runMode:beforeDate: reviendrait avec un booléen, le while() serait à nouveau exécuté, il détecterait que shouldKeepRunning a été défini à NO , et la boucle se terminerait.

Avec votre raffinement, le runMode:beforeDate: retournera dans les 0,1 secondes, qu’il ait ou non des sources d’entrée associées ou qu’il ait traité des entrées. C’est une supposition éclairée (je ne suis pas un expert sur les internes de la boucle d’exécution), mais pensez que votre raffinement est le bon moyen de gérer la situation.

Votre deuxième exemple consiste simplement à contourner le sondage pour vérifier l’entrée de la boucle d’exécution dans l’intervalle de temps 0.1.

Parfois, je trouve une solution pour votre premier exemple:

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