web-dev-qa-db-fra.com

La meilleure façon de faire attendre NSRunLoop pour qu'un drapeau soit défini?

Dans la documentation Apple pour NSRunLoop , il y a un exemple de code démontrant la suspension de l'exécution en attendant qu'un indicateur soit défini par autre chose.

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

J'ai utilisé cela et cela fonctionne, mais en enquêtant sur un problème de performances, je l'ai trouvé 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 que le drapeau soit défini (dans une autre méthode) puis une ligne après while() il y a une attente apparemment aléatoire entre les deux instructions de log de plusieurs secondes.

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

J'ai contourné ce problème avec le code suivant, mais il ne semble pas juste 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];

à l'aide de ce code, les instructions du journal lors de la définition de l'indicateur et après la boucle while sont désormais systématiquement espacées de moins de 0,1 seconde.

Quelqu'un a-t-il des idées pour lesquelles le code d'origine présente ce comportement?

39
Dave Verwer

Runloops peut ê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. OR retourne s'il ne traite aucun événement avant que le délai d'expiration ne soit atteint.

Avec un délai d'expiration de 0,1 seconde, vous dépassez le délai le plus souvent. Le runloop se déclenche, ne traite aucun événement et revient en 0,1 seconde. Parfois, il aura la possibilité de traiter un événement.

Avec votre timeout distantFuture, le runloop attendra toujours jusqu'à ce qu'il traite un événement. Donc, quand il vous revient, il vient de traiter un événement quelconque.

Une valeur de délai court consommera considérablement plus de CPU que le délai infini, mais il existe de bonnes raisons d'utiliser un délai court, par exemple si vous souhaitez mettre fin au processus/thread d'exécution de la boucle d'exécution. Vous souhaiterez probablement que la boucle d'exécution le remarque qu'un drapeau a changé et qu'il doit renflouer dès que possible.

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

Voir this Apple doc pour plus d'informations.

36
schwa

D'accord, 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 une boucle d'exécution interne. Il y restera jusqu'à ce que l'autre thread annonce que c'est fait. Pour réveiller le thread principal, cela lui fera traiter une fonction sans autre but que de réveiller le thread principal.

Ce n'est qu'une façon de procéder. Une notification publiée et traitée sur le thread principal pourrait être préférable (d'autres threads pourraient également s'enregistrer), mais la solution ci-dessus est la plus simple à laquelle je puisse penser. BTW ce n'est pas vraiment thread-safe. Pour être vraiment sûr pour les threads, chaque accès au booléen doit être verrouillé par un objet NSLock à partir de l'un ou l'autre thread (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; le Le standard C ne connaît cependant pas les verrous, donc ici seul volatile peut garantir que ce code fonctionne; GCC n'a pas besoin que volatile soit défini pour une variable protégée par des verrous).

15
Mecki

En général, si vous traitez vous-même des événements en boucle, vous vous trompez. Selon mon expérience, cela peut causer une tonne de problèmes désordonnés.

Si vous souhaitez exécuter de façon modale - par exemple, afficher un panneau de progression - exécuter de manière modale! Allez-y et utilisez les méthodes NSApplication, exécutez modalement pour 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 qu'une vue soit active pendant la durée de votre 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), 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 de course!

-Wil

11
Wil Shipley

Si vous voulez pouvoir définir votre variable indicateur et que la boucle d'exécution le remarque immédiatement, 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 entraînera la rotation immédiate de votre boucle d'exécution, la méthode à invoquer, puis l'indicateur sera vérifié.

10
Chris Hanson

À votre code, le thread actuel 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. 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? Il y restera, jusqu'à ce qu'il obtienne un autre événement à traiter, puis il en sortira. Cela peut arriver en 1, 3, 5, 10 ou même 20 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é. IOW le code Apple que vous avez cité est indéterministe. Cet exemple ne fonctionner 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 est nommée 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 démarrer 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 devez publier de manière asynchrone une notification qui est traitée dans le thread principal et exécute le code qui doit s'exécuter dès que le chargement est terminé.

5
Mecki

J'ai rencontré des problèmes similaires en essayant de gérer NSRunLoops. discussion pour runMode:beforeDate: Sur la page des références de classe indique:

Si aucune source d'entrée ou minuterie n'est attachée à la boucle d'exécution, cette méthode se ferme immédiatement; sinon, il retourne après que la première source d'entrée a été traitée ou que limitDate est atteint. La suppression manuelle de toutes les sources d'entrée et temporisateurs connus de la boucle d'exécution n'est pas une garantie que la boucle d'exécution se terminera. Mac OS X peut installer et supprimer des sources d'entrée supplémentaires selon les besoins pour traiter les demandes ciblées sur le thread du destinataire. Ces sources pourraient donc empêcher la boucle d'exécution de se terminer.

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: Se bloque jusqu'à ce que cette source d'entrée soit traitée, soit supprimée. Dans votre cas, cela prenait " quelques secondes et jusqu'à 10 secondes " pour que cela se produise, moment auquel runMode:beforeDate: Reviendrait avec un booléen, la while() s'exécuterait à nouveau, elle détecterait que shouldKeepRunning a été définie sur NO et la boucle se terminerait.

Avec votre raffinement, le runMode:beforeDate: Reviendra dans les 0,1 secondes, qu'il ait ou non joint des sources d'entrée ou traité une entrée. C'est une supposition éclairée (je ne suis pas un expert des internes de la boucle d'exécution), mais pensez que votre raffinement est la bonne façon de gérer la situation.

2
Jon Shea

Votre deuxième exemple fonctionne simplement pendant que vous interrogez 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]]);
0
Richard