Best way to make NSRunLoop wait for a flag to be set?
在Apple的NSRELOOP文档中,有一个示例代码演示了在等待其他对象设置标志时暂停执行。
1 2 3 | BOOL shouldKeepRunning = YES; // global NSRunLoop *theRL = [NSRunLoop currentRunLoop]; while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); |
我一直在使用它,它是有效的,但在调查性能问题时,我把它追踪到了这段代码。我使用几乎完全相同的代码(只是标记的名称不同),如果我在设置标记后(在另一种方法中)在行上放置一个
在速度较慢或更快的机器上,延迟似乎没有什么不同,但在不同的运行中,延迟至少为几秒到10秒。
我用下面的代码解决了这个问题,但是原始代码不起作用似乎是不对的。
1 2 3 | NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1]; while (webViewIsLoading && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil]) loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1]; |
使用此代码,当设置标志和while循环之后的log语句之间的间隔始终小于0.1秒。
有人知道原始代码为什么会表现出这种行为吗?
runloops可以是一个有点神奇的盒子,在这里事情会发生。
基本上,您要告诉runloop去处理一些事件,然后返回。或者,如果在超时之前它不处理任何事件,则返回。
当超时时间为0.1秒时,您会更频繁地调用超时。runloop触发,不处理任何事件,并在0.1秒后返回。偶尔会有机会处理一个事件。
有了远程未来超时,runloop将一直等待,直到它处理一个事件。所以当它返回给您时,它只是处理了某种类型的事件。
短超时值将比无限超时消耗更多的CPU,但有充分的理由使用短超时,例如,如果您希望终止运行循环的进程/线程。您可能希望runloop注意到一个标志已经更改,并且它需要尽快退出。
您可能希望使用runloop观察器,这样您就可以确切地看到runloop在做什么。
有关更多信息,请参阅此Apple文档。
好吧,我向你解释了这个问题,这里有一个可能的解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | @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显示进度指示器并捕获内部runloop中的主线程。它将一直保持在那里,直到另一个线程宣布它已经完成。为了唤醒主线程,它将使它处理一个除了唤醒主线程之外没有任何目的的函数。
这只是你能做到的方法之一。在主线程上发布和处理通知可能更可取(其他线程也可以为此注册),但上面的解决方案是我能想到的最简单的方法。顺便说一句,它不是真正的线程安全的。要真正保证线程安全,每个对布尔值的访问都需要由nslock对象从任一线程锁定(使用这样的锁也会使"volatile"过时,因为受锁保护的变量根据posix标准是隐式volatile;但是C标准不知道锁,因此这里只有volatile可以保证该代码工作;GCC不需要为受锁保护的变量设置volatile)。
一般来说,如果您自己在一个循环中处理事件,那么这是错误的。以我的经验来看,这会造成很多麻烦。
如果您想运行模式——例如,显示进度面板——运行模式!继续使用nsapplication方法,以模式运行进度表,然后在加载完成后停止模式。请参阅Apple文档,例如http://developer.apple.com/documentation/cocoa/conceptive/winpanel/concepts/usingmodalwindows.html。
如果您只想让视图在加载期间处于开启状态,但不希望它是模态的(例如,您希望其他视图能够响应事件),那么您应该做一些更简单的事情。例如,您可以这样做:
1 2 3 4 5 6 7 8 9 10 11 | - (IBAction)start:(id)sender { pageStillLoading = YES; [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil]; [progress setHidden:NO]; } - (void)wakeUpMainThreadRunloop:(id)arg { [progress setHidden:YES]; } |
你完成了。不需要控制运行循环!
-威尔
如果希望能够设置标志变量并立即注意到运行循环,只需使用
在代码处,当前线程将每隔0.1秒检查变量是否已更改。在Apple代码示例中,更改变量不会有任何效果。runloop将一直运行,直到它处理某个事件。如果webviewisloading的值发生了变化,则不会自动生成任何事件,因此它将保持在循环中,为什么它会中断?它将停留在那里,直到它得到一些其他的事件处理,然后它将爆发出来。这可能在1、3、5、10甚至20秒内发生。在这种情况发生之前,它不会脱离runloop,因此不会注意到这个变量已经改变。你引用的苹果代码是不确定的。只有当webviewisloading的值更改也创建了一个导致runloop唤醒的事件时,此示例才会起作用,而事实似乎并非如此(或至少并非总是如此)。
我认为你应该重新考虑这个问题。由于变量名为webviewisloading,是否等待加载网页?你用WebKit吗?我怀疑您根本不需要这样的变量,也不需要您发布的任何代码。相反,您应该异步编写应用程序代码。您应该启动"网页加载过程",然后返回主循环,一旦页面完成加载,您应该异步发布一个在主线程中处理的通知,并运行加载完成后应该运行的代码。
我在管理
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it returns after either the first input source is processed or limitDate is reached. Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. Mac OS X may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
我的最佳猜测是,一个输入源连接到您的
通过您的改进,
您的第二个示例只是在轮询以检查运行循环在时间间隔0.1内的输入时使用。
偶尔我会为您的第一个示例找到一个解决方案:
1 2 3 | BOOL shouldKeepRunning = YES; // global NSRunLoop *theRL = [NSRunLoop currentRunLoop]; while (shouldKeepRunning && [theRL runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]); |