让NSRunLoop等待设置标志的最佳方法是什么?

时间:2008-09-29 17:00:58

标签: objective-c cocoa macos

NSRunLoop的Apple文档中,有一些示例代码,用于说明在等待标记由其他内容设置时暂停执行。

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

我一直在使用它并且它有效,但在调查性能问题时,我将其跟踪到这段代码。我使用几乎完全相同的代码片段(只是标志的名称是不同的:)如果我在设置标志后放置一个NSLog(在另一个方法中)然后在while()在几秒钟的两个日志语句之间有一个看似随机的等待。

在较慢或较快的机器上,延迟似乎没有什么不同,但在不同运行之间的延迟时间至少为几秒到10秒。

我使用以下代码解决了这个问题,但原始代码不起作用似乎不正确。

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

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

有任何想法为什么原始代码表现出这种行为?

7 个答案:

答案 0 :(得分:36)

Runloops可能是一个神奇的盒子,只会发生一些事情。

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

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

在您的remoteFuture超时后,runloop将等待,直到它处理一个事件。因此当它返回给你时,它刚刚处理了某种事件。

短超时值将比无限超时消耗更多的CPU,但是有充分的理由使用短超时,例如,如果要终止运行runloop的进程/线程。您可能希望runloop注意到一个标志已经改变,它需要尽快摆脱困境。

您可能想要使用runloop观察者,这样您就可以准确地看到runloop正在做什么。

有关详细信息,请参阅this Apple doc

答案 1 :(得分:15)

好的,我向您解释了问题,这是一个可能的解决方案:

@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来保护锁的变量。

答案 2 :(得分:11)

一般来说,如果你自己在一个循环中处理事件,你就是在做错了。根据我的经验,它可能会导致大量混乱的问题。

如果你想以模态方式运行 - 例如,显示进度面板 - 以模态方式运行!继续使用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];
}

你已经完成了。无需控制运行循环!

-Wil

答案 3 :(得分:10)

如果您希望能够设置标志变量并立即注意运行循环,只需使用-[NSRunLoop performSelector:target:argument:order:modes:来请求运行循环调用将标志设置为false的方法。这将导致您的运行循环立即旋转,要调用的方法,然后将检查该标志。

答案 4 :(得分:5)

在您的代码中,当前线程将检查变量是否每0.1秒更改一次。在Apple代码示例中,更改变量不会产生任何影响。 runloop将运行直到它处理某个事件。如果webViewIsLoading的值已经更改,则不会自动生成任何事件,因此它将保持循环,为什么会突破它?它将留在那里,直到它得到一些其他事件来处理,然后它将突破它。这可能发生在1,3,5,10或甚至20秒。在此之前,它不会突破runloop,因此它不会注意到这个变量已经改变。 Iow你引用的Apple代码是不确定的。此示例仅在webViewIsLoading的值更改还会创建导致runloop唤醒的事件时才会起作用,而且似乎并非如此(或者至少并非总是如此)。

我认为你应该重新考虑这个问题。由于您的变量名为webViewIsLoading,您是否等待加载网页?您是否正在使用Webkit?我怀疑你根本不需要这样的变量,也不需要你发布的任何代码。相反,您应该异步编写应用程序代码。您应该启动“网页加载过程”,然后返回主循环,一旦页面加载完毕,您应该异步发布在主线程中处理的通知并运行应该尽快运行的代码装货已经完成。

答案 5 :(得分:2)

我在尝试管理NSRunLoops时遇到了类似的问题。类引用页面上runMode:beforeDate:的{​​{3}}表示:

  

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

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

通过细化,runMode:beforeDate:将在0.1秒内返回,无论它是否附加了输入源或已处理任何输入。这是一个有根据的猜测(我不是运行循环内部的专家),但认为你的改进是处理这种情况的正确方法。

答案 6 :(得分:0)

您的第二个示例只是在轮询时解决,以便在0.1时间间隔内检查运行循环的输入。

我偶尔会找到第一个例子的解决方案:

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