使NSRunLoop等待标志被设置的最佳方法?

在NSRunLoop的Apple文档中,有一些示例代码演示了在等待一个标志被其他东西设置的同时暂停执行。

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

我一直在使用它,它的工作原理,但在调查性能问题,我跟踪到这段代码。 我使用几乎完全相同的一段代码(只是标志的名称是不同的:),如果我在标志被设置(在另一种方法),然后在while()之后的一行后放一个NSLog上线是几秒钟之间的两个日志语句之间看似随机的等待。

延迟似乎没有在慢或更快的机器上有所不同,但从运行到运行至less有几秒钟和多达10秒。

我已经用下面的代码解决了这个问题,但是原来的代码不起作用。

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

使用此代码,设置标志和while循环之后的日志语句现在始终小于0.1秒。

任何人有任何想法,为什么原始代码展示这种行为

runloops可以是一个魔术盒,只是发生的东西。

基本上你要告诉runloop去处理一些事件然后返回。 如果在超时命中之前没有处理任何事件,则返回。

在0.1秒的超时时间内,你经常发生超时。 runloop触发,不处理任何事件并在0.1秒内返回。 偶尔会有机会处理一个事件。

随着你遥远的未来超时,runloop将等待,直到它处理一个事件。 所以当它回到你的时候,它刚刚处理了某种事件。

短暂的超时值会比无限的超时消耗更多的CPU,但是使用短暂超时有很好的理由,例如,如果你想终止runloop运行的进程/线程,你可能会希望runloop注意到国旗已经改变,需要尽快解救。

你可能想玩runloop观察者,所以你可以看到runloop正在做什么。

请参阅此Apple文档以获取更多信息。

好的,我向你解释了这个问题,这里有一个可能的解决scheme:

 @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 

开始显示进度指示器并捕获内部runloop中的主线程。 它会留在那里,直到另一个线程宣布它已经完成。 唤醒主线程,除了唤醒主线程以外,它将使其无需处理任何function。

这只是一个方法,你可以做到这一点。 在主线程上发布和处理通知可能更可取(其他线程也可以注册),但上面的解决scheme是我能想到的最简单的方法。 顺便说一句,它不是真的线程安全的。 为了确实是线程安全的,每个对布尔的访问都需要被任一线程的一个NSLock对象locking(使用这样的一个锁也会使“volatile”变得过时,因为受锁保护的variables是隐式的,符合POSIX标准; C标准却并不知道锁,所以这里只有volatile才能保证这个代码工作; GCC不需要volatile来设置一个被锁保护的variables)。

一般来说,如果你正在循环处理事件,那么你就是在做错了。 根据我的经验,这可能会导致一堆麻烦的问题。

如果你想运行模态 – 例如,显示进度面板 – 模态运行! 继续使用NSApplication方法,以模态方式运行进度表,然后在加载完成时停止模态。 请参阅Apple文档,例如http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html

如果你只是想要一个视图在你的负载的时间,但你不希望它是模态(例如,你希望其他视图能够响应事件),那么你应该做一些简单的事情。 例如,你可以这样做:

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

你完成了。 无需保持运行循环的控制!

-会

如果你想能够设置你的标志variables,并立即注意到运行循环,只需使用-[NSRunLoop performSelector:target:argument:order:modes:要求运行循环调用设置标志为false的方法。 这将导致你的运行循环立即旋转,被调用的方法,然后标志将被检查。

在您的代码中,当前线程将检查每0.1秒更改一次的variables。 在Apple代码示例中,更改variables不会有任何影响。 runloop将运行,直到它处理一些事件。 如果webViewIsLoading的值发生了变化,那么没有事件会自动生成,因此它会停留在循环中,为什​​么会跳出来呢? 它会留在那里,直到其他事件处理,然后它会打破它。 这可能发生在1,3,5,10甚至20秒。 直到发生这种情况,它不会跳出runloop,因此它不会注意到这个variables已经改变了。 IOW你引用的苹果代码是不确定的。 这个例子只有在webViewIsLoading的值改变时才会产生一个导致runloop醒来的事件,这似乎不是这种情况(或者至less并不总是)。

我认为你应该重新思考这个问题。 由于您的variables名为webViewIsLoading,您是否等待网页加载? 你在使用Webkit吗? 我怀疑你是否需要这样一个variables,也没有你发布的任何代码。 相反,你应该编写你的应用程序asynchronous。 你应该开始“网页加载过程”,然后返回到主循环,一旦页面完成加载,你应该asynchronous发布在主线程中处理的通知,并运行尽快运行的代码加载完成。

我试图pipe理NSRunLoops时遇到过类似的问题。 runMode:beforeDate:的讨论 runMode:beforeDate:在类引用页面上说:

如果没有input源或定时器附加到运行循环,则此方法立即退出; 否则,在处理第一个input源或达到limitDate后返回。 手动从运行循环中删除所有已知的input源和定时器并不能保证运行循环将退出。 Mac OS X可以根据需要安装和删除额外的input源,以处理接收者线程的请求。 这些来源可以阻止运行循环退出。

我最好的猜测是一个input源被附加到你的NSRunLoop ,可能是OS X本身,并且runMode:beforeDate:被阻塞,直到input源有一些input被处理,或被删除。 在你的情况下,这是“ 几秒钟,最多10秒 ”发生,在这一点runMode:beforeDate:将返回一个布尔值, while()将再次运行,它会检测到应该shouldKeepRunning已经设置到NO ,循环将终止。

随着您的细化, runMode:beforeDate:将在0.1秒内返回,无论它是否已连接input源或已处理任何input。 这是一个有教养的猜测(我不是运行循环内部的专家),但认为你的改进是处理这种情况的正确方法。

你的第二个例子只是在你轮询的时候,在时间间隔0.1内检查运行循环的input。

偶尔我会为你的第一个例子find一个解决scheme:

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