39 votes

Meilleur moyen de faire attendre NSRunLoop pour qu'un drapeau soit activé ?

Dans la documentation d'Apple pour NSRunLoop il existe un exemple de code démontrant la suspension de l'exécution en attendant qu'un drapeau soit activé par quelque chose d'autre.

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

J'ai utilisé cette méthode et elle fonctionne, mais en examinant un problème de performance, j'ai trouvé la cause de ce morceau de code. J'utilise presque exactement le même morceau de code (seul le nom du drapeau est différent :) et si je mets un fichier NSLog sur la ligne qui suit la mise en place de l'indicateur (dans une autre méthode) et ensuite une ligne après l'indicateur while() il y a une attente apparemment aléatoire de plusieurs secondes entre les deux relevés de journal.

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

J'ai contourné ce problème avec le code suivant mais il ne semble pas normal que le code original 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 déclarations de journal lors de l'activation du drapeau et après la boucle while sont maintenant systématiquement espacées de moins de 0,1 seconde.

Quelqu'un a-t-il une idée de la raison pour laquelle le code original présente ce comportement ?

36voto

schwa Points 9102

Les Runloops sont un peu comme une boîte magique où tout se passe.

En fait, vous dites à la boucle d'exécution d'aller traiter quelques événements et de revenir. OU de revenir si elle 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, il est plus fréquent que le délai d'attente ne soit pas respecté. La boucle d'exécution se déclenche, ne traite aucun événement et revient dans 0,1 seconde. Occasionnellement, il aura la chance de traiter un événement.

Avec votre délai d'attente distantFuture, la boucle d'exécution attendra éternellement jusqu'à ce qu'elle traite un événement. Ainsi, lorsqu'il revient vers vous, il vient de traiter un événement quelconque.

Une valeur de timeout courte consommera considérablement plus de CPU que le timeout infini mais il y a de bonnes raisons d'utiliser un timeout court, par exemple si vous voulez terminer le processus/thread dans lequel le runloop est exécuté. Vous voudrez probablement que le runloop remarque qu'un drapeau a changé et qu'il doit se retirer au plus vite.

Vous pouvez jouer avec les observateurs de boucle d'exécution afin de voir exactement ce que fait la boucle d'exécution.

Voir ce document Apple pour plus d'informations.

15voto

Mecki Points 35351

Bon, 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

commencer affiche un indicateur de progression et capture le thread principal dans une boucle d'exécution interne. Il y restera jusqu'à ce que l'autre thread annonce qu'il a terminé. Pour réveiller le thread principal, il lui fera traiter une fonction sans autre but que de réveiller le thread principal.

Ce n'est qu'une façon parmi d'autres de le faire. Une notification postée et traitée sur le fil principal pourrait être préférable (d'autres fils pourraient également s'y inscrire), mais la solution ci-dessus est la plus simple à laquelle je peux penser. BTW il n'est pas vraiment thread-safe. Pour être vraiment thread-safe, chaque accès au booléen doit être verrouillé par un objet NSLock depuis l'un ou l'autre des threads (l'utilisation d'un tel verrou rend également "volatile" obsolète, car les variables protégées par un verrou sont implicitement volatiles selon la norme POSIX ; la norme C ne connaît cependant pas les verrous, donc ici seul volatile peut garantir le fonctionnement de ce code ; GCC n'a pas besoin que volatile soit défini pour une variable protégée par des verrous).

11voto

Wil Shipley Points 5327

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

Si vous voulez fonctionner de manière modale - par exemple, en affichant un panneau de progression - exécutez-la de manière modale ! Allez-y et utilisez les méthodes NSApplication, exécutez modalement pour la feuille de progression, puis arrêtez la modale 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 simplement qu'une vue soit affichée pendant la durée du chargement, mais que vous ne voulez pas qu'elle soit modale (par exemple, vous voulez que d'autres vues puissent répondre aux événements), vous devez faire quelque chose de beaucoup plus simple. Par exemple, vous pourriez 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 vous avez terminé. Plus besoin de garder le contrôle de la boucle d'exécution !

-Wil

10voto

Chris Hanson Points 34485

Si vous voulez être capable de définir votre variable drapeau et que la boucle d'exécution le remarque immédiatement, utilisez simplement [-[NSRunLoop performSelector:target:argument:order:modes:](http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSRunLoop_Class/Reference/Reference.html#//apple_ref/doc/uid/20000321-performSelector_target_argument_order_modes_) pour demander à la boucle d'exécution d'invoquer la méthode qui met le drapeau à faux. Ainsi, la boucle d'exécution tournera immédiatement, la méthode sera invoquée, puis le drapeau sera vérifié.

5voto

Mecki Points 35351

Dans votre code, le thread actuel vérifiera que la variable a changé toutes les 0,1 secondes. Dans l'exemple du code Apple, le changement de la variable n'aura aucun effet. La boucle d'exécution s'exécutera jusqu'à ce qu'elle traite un événement. Si la valeur de webViewIsLoading a changé, aucun événement n'est généré automatiquement, donc il restera dans la boucle, pourquoi en sortirait-il ? Elle y restera, jusqu'à ce qu'elle reçoive un autre événement à traiter, puis elle en sortira. Cela peut se produire dans 1, 3, 5, 10 ou même 20 secondes. Et jusqu'à ce que cela se produise, il ne sortira pas de la boucle d'exécution et ne remarquera donc pas que cette variable a changé. Autrement dit, le code Apple que vous avez cité est indéterministe. Cet exemple ne fonctionnera que si le changement de valeur de webViewIsLoading crée également un événement qui provoque le réveil de la boucle d'exécution et cela ne semble 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 d'aucun des codes que vous avez postés. Vous devriez plutôt coder votre application de manière asynchrone. Vous devriez lancer le "processus de chargement de la page web", puis revenir à la boucle principale et, dès que le chargement de la page est terminé, vous devriez poster de manière asynchrone une notification qui est traitée dans le thread principal et exécute le code qui devrait s'exécuter dès que le chargement est terminé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X