2008-09-29 4 views
39

In der Apple-Dokumentation für NSRunLoop gibt es einen Beispielcode, der zeigt, dass die Ausführung ausgesetzt wird, während auf das Setzen eines Flags durch etwas anderes gewartet wird.Die beste Möglichkeit, NSRunLoop auf das Setzen eines Flags warten zu lassen?

Ich habe dies verwendet und es funktioniert, aber bei der Untersuchung eines Leistungsproblems habe ich es auf dieses Stück Code aufgespürt. Ich benutze fast genau das gleiche Stück Code (nur der Name der Flagge ist anders :) und wenn ich eine NSLog in die Zeile setzen, nachdem das Flag gesetzt ist (in einer anderen Methode) und dann eine Zeile nach der while() gibt es eine scheinbar zufällig zwischen den beiden Log-Anweisungen von mehreren Sekunden warten.

Die Verzögerung scheint auf langsameren oder schnelleren Maschinen nicht anders zu sein, variiert aber von Lauf zu Lauf, wobei sie mindestens ein paar Sekunden und bis zu 10 Sekunden beträgt.

Ich habe dieses Problem mit dem folgenden Code bearbeitet, aber es scheint nicht richtig, dass der ursprüngliche Code nicht funktioniert.

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

diesen Code verwenden, werden die Protokollanweisungen, wenn das Flag gesetzt und nach der while-Schleife sind heute durchweg weniger als 0,1 Sekunden auseinander.

Wer irgendwelche Ideen, warum der ursprüngliche Code zeigt dieses Verhalten?

+0

Ich denke, dass Sie das Beispiel in der Dokumentation gegeben falsch verstanden; Die Verwendung dieses Beispielcodes ist, wenn Sie die Ausführung eines Runloops beenden möchten (um nicht über die Flag-Änderung benachrichtigt zu werden) https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/ NSRunLoop_Class/Reference/Reference.html # // Apple_ref/occ/instm/NSRunLoop/run – kernix

Antwort

35

Runloops können ein bisschen wie eine Zauberkiste sein, in der Dinge einfach passieren.

Im Grunde sagen Sie dem Runloop, einige Ereignisse zu verarbeiten und dann zurückzukehren. ODER zurückgeben, wenn keine Ereignisse vor dem Timeout verarbeitet werden.

Mit einem Zeitlimit von 0,1 Sekunden wird die Zeitüberschreitung häufiger als nicht angenommen. Der Runloop feuert, verarbeitet keine Ereignisse und kehrt in 0,1 Sekunden zurück. Gelegentlich wird es eine Chance bekommen, ein Ereignis zu verarbeiten.

Mit Ihrem fistFuture-Timeout wartet der Runloop immer weiter, bis er ein Ereignis verarbeitet. Wenn es zu dir zurückkehrt, hat es gerade ein Ereignis verarbeitet.

Ein kurzer Timeout-Wert verbraucht wesentlich mehr CPU als das infinite-Timeout, aber es gibt gute Gründe, ein kurzes Timeout zu verwenden, zB wenn Sie den Prozess/Thread beenden möchten, in dem der Runloop läuft der Runloop, um zu bemerken, dass sich eine Flagge geändert hat und dass sie so schnell wie möglich aus der Patsche gehen muss.

Sie möchten vielleicht mit Runloop-Beobachtern experimentieren, damit Sie genau sehen können, was der Runloop macht.

Weitere Informationen finden Sie unter this Apple doc.

2

Ich hatte ähnliche Probleme beim Versuch, NSRunLoops zu verwalten. Die discussion für runMode:beforeDate: auf der Klasse Referenzen Seite sagt:

Wenn keine Eingangsquellen oder Timer zur Laufschleife gebunden sind, ist dieses Verfahren beendet sofort; Andernfalls kehrt es zurück, nachdem entweder die erste Eingabequelle verarbeitet oder limitDate erreicht wurde. Das manuelle Entfernen aller bekannten Eingabequellen und Timer aus der Laufschleife garantiert nicht, dass die Laufschleife beendet wird. Mac OS X kann nach Bedarf zusätzliche Eingabequellen installieren und entfernen, um Anforderungen zu verarbeiten, die auf den Thread des Empfängers ausgerichtet sind. Diese Quellen könnten daher verhindern, dass die Laufschleife endet.

Meine beste Vermutung ist, dass eine Eingangsquelle an Ihrem NSRunLoop angebracht ist, vielleicht von OS X selbst, und dass runMode:beforeDate: wird, bis diese Eingangsquelle blockiert entweder hat einige Eingang bearbeitet oder entfernt wird. In Ihrem Fall war es für diese „paar Sekunden und bis zu 10 Sekunden“ nehmen, an denen geschehen, Punkt runMode:beforeDate: mit einem boolean zurückkehren würde, würde die while() wieder läuft, würde es erkennen, dass shouldKeepRunning-NO gesetzt wurde, und die Schleife würde enden.

Mit Ihrer Verfeinerung kehrt die runMode:beforeDate: innerhalb von 0,1 Sekunden zurück, unabhängig davon, ob sie Eingangsquellen angeschlossen hat oder irgendwelche Eingaben verarbeitet hat. Es ist eine fundierte Vermutung (ich bin kein Experte auf der Laufschleife Interna), aber denke, dass Ihre Verfeinerung der richtige Weg ist, um mit der Situation umzugehen.

10

Wenn Sie in der Lage sein möchten, Ihre Flagvariable zu setzen und die Laufschleife sofort zu bemerken, verwenden Sie einfach -[NSRunLoop performSelector:target:argument:order:modes:, um die Laufschleife aufzufordern, die Methode aufzurufen, die das Flag auf false setzt. Dies führt dazu, dass Ihre Run-Schleife sofort gedreht wird, die Methode, die aufgerufen wird, und dann wird das Flag überprüft.

+1

Danke Chris, leider wird dies nicht funktionieren, da das Flag in einer Delegate-Methode auf WebView gesetzt wird, so dass ich es nicht sofort aufrufen kann. –

5

Bei Ihrem Code überprüft der aktuelle Thread, ob sich die Variable alle 0,1 Sekunden geändert hat. Im Apple-Codebeispiel hat das Ändern der Variablen keine Auswirkung. Der Runloop wird ausgeführt, bis er ein Ereignis verarbeitet. Wenn sich der Wert von webViewIsLoading geändert hat, wird kein Ereignis automatisch generiert, also bleibt es in der Schleife, warum würde es ausbrechen? Es wird dort bleiben, bis es ein anderes Ereignis zu verarbeiten bekommt, dann wird es ausbrechen. Dies kann in 1, 3, 5, 10 oder sogar 20 Sekunden geschehen. Und bis das passiert, wird es nicht aus der Runloop ausbrechen und somit wird es nicht bemerken, dass sich diese Variable geändert hat. IOW Der von Ihnen zitierte Apple-Code ist indeterministisch. Dieses Beispiel funktioniert nur, wenn die Wertänderung von webViewIsLoading auch ein Ereignis erzeugt, das dazu führt, dass der Runloop aufwacht und dies nicht der Fall zu sein scheint (oder zumindest nicht immer).

Ich denke, Sie sollten das Problem überdenken. Da Ihre Variable webViewIsLoading heißt, warten Sie, bis eine Webseite geladen wurde? Verwenden Sie Webkit dafür? Ich bezweifle, dass Sie überhaupt eine solche Variable oder einen von Ihnen geposteten Code benötigen. Stattdessen sollten Sie Ihre App asynchron codieren. Sie sollten den "Webseitenladevorgang" starten und dann zur Hauptschleife zurückkehren und sobald die Seite geladen wurde, sollten Sie eine Benachrichtigung, die im Hauptthread verarbeitet wird, asynchron posten und den Code ausführen, der sofort ausgeführt werden soll Laden ist beendet.

+1

Dies ist eine großartige Erklärung, warum der RunLoop nicht ausbrach, Danke. Es gibt einen guten Grund, warum das Laden von WebView modal sein muss und ich muss es so halten. –

15

Okay, ich erklärte das Problem, hier ist eine mögliche Lösung:

@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 zeigt eine Fortschrittsanzeige und fängt den Haupt-Thread in einem internen Runloop. Es wird dort bleiben, bis der andere Thread ankündigt, dass es fertig ist. Um den Haupt-Thread aufzuwecken, wird er eine Funktion ausführen, die keinen anderen Zweck hat, als den Haupt-Thread aufzuwecken.

Dies ist nur eine Möglichkeit, wie Sie es tun können. Eine Benachrichtigung, die auf dem Hauptthread veröffentlicht und verarbeitet wird, ist möglicherweise vorzuziehen (auch andere Threads könnten dafür registriert werden), aber die obige Lösung ist die einfachste, die ich mir vorstellen kann. Übrigens ist es nicht wirklich threadsicher. Um threadsicher zu sein, muss jeder Zugriff auf den booleschen Wert durch ein NSLock-Objekt aus beiden Threads gesperrt werden (das Verwenden einer solchen Sperre macht auch "flüchtig" obsolet, da die durch eine Sperre geschützten Variablen implizit flüchtig nach dem POSIX-Standard sind; Der C-Standard weiß jedoch nichts über Sperren, daher kann hier nur volatile dafür sorgen, dass dieser Code funktioniert, und GCC benötigt kein volatile für eine Variable, die durch Sperren geschützt wird.

+0

Sehr cool. Dies fügt meinem Objc-Arsenal ein neues Werkzeug hinzu. Erwäge, deine Technik zu [diesem Thread] hinzuzufügen [1] [1]: http://stackoverflow.com/questions/155964/what-are-best-practices-that-you-use-with-writing-objective- c-and-cacao # 156343 – schwa

+0

Tolle Technik, sehr elegant. –

+0

Ich möchte wissen, ob der UIButton (Absender) den Status UIControlStateSelected für 5 Sekunden hält? – user501836

11

Im Allgemeinen, wenn Sie Ereignisse selbst in einer Schleife verarbeiten, tun Sie es falsch. Es kann eine Menge unordentlicher Probleme nach meiner Erfahrung verursachen.

Wenn du modal laufen willst - zum Beispiel ein Fortschrittspanel anzeigen - laufe modal! Fahren Sie fort und verwenden Sie die NSApplication-Methoden, führen Sie sie modal für das Fortschrittsblatt aus, und stoppen Sie dann das Modal, wenn der Ladevorgang abgeschlossen ist. Siehe die Apple-Dokumentation, zum Beispiel http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html.

Wenn Sie nur möchten, dass eine Ansicht für die Dauer Ihrer Auslastung verfügbar ist, aber nicht modal sein soll (z. B. möchten Sie, dass andere Ansichten auf Ereignisse reagieren können), sollten Sie dies tun etwas viel einfacheres. Zum Beispiel könnten Sie das tun:

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

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

Und du bist fertig. Keine Notwendigkeit, die Kontrolle über die Laufschleife zu behalten!

-Wil

0

Ihr zweites Beispiel arbeiten gerade um, wie Sie Umfrage Eingang des Laufschleife innerhalb des Zeitintervalls 0,1 zu überprüfen.

Gelegentlich finde ich eine Lösung für Ihr erstes Beispiel:

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