40

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 秒。

有人知道为什么原始代码会表现出这种行为吗?

4

7 回答 7

36

Runloops 可能有点像一个神奇的盒子,事情发生了。

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

使用 0.1 秒超时,您经常会遇到超时。runloop 触发,不处理任何事件并在 0.1 秒内返回。偶尔它会有机会处理一个事件。

随着您的 distinctFuture 超时,runloop 将永远等待,直到它处理一个事件。因此,当它返回给您时,它刚刚处理了某种事件。

短超时值将比无限超时消耗更多的 CPU,但使用短超时有充分的理由,例如,如果您想终止 runloop 正在运行的进程/线程。您可能希望 runloop 注意到旗帜已经改变,需要尽快救助。

你可能想玩弄一下 runloop 观察者,这样你就可以准确地看到 runloop 正在做什么。

有关更多信息,请参阅此 Apple 文档

于 2008-09-29T19:40:03.957 回答
17

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

@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显示进度指示器并在内部运行循环中捕获主线程。它会一直呆在那里,直到另一个线程宣布它完成。为了唤醒主线程,它将使它处理一个除了唤醒主线程之外没有其他目的的函数。

这只是您可以做到的一种方式。在主线程上发布和处理通知可能更可取(其他线程也可以注册它),但上面的解决方案是我能想到的最简单的解决方案。顺便说一句,它并不是真正的线程安全。为了真正实现线程安全,对布尔值的每次访问都需要由任一线程的 NSLock 对象锁定(使用这样的锁也会使“易失性”过时,因为受锁保护的变量根据 POSIX 标准是隐式易失性的;然而,C 标准不知道锁,所以这里只有 volatile 可以保证这个代码工作;GCC 不需要为受锁保护的变量设置 volatile)。

于 2008-10-07T11:44:39.603 回答
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];
}

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

-会

于 2008-10-25T22:52:19.743 回答
10

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

于 2008-09-30T02:09:30.230 回答
5

在您的代码中,当前线程将检查变量是否每 0.1 秒更改一次。在 Apple 代码示例中,更改变量不会产生任何影响。runloop 将一直运行,直到它处理一些事件。如果 webViewIsLoading 的值发生了变化,不会自动产生事件,所以会一直循环,为什么会跳出来呢?它会一直呆在那里,直到它得到一些其他事件来处理,然后它会打破它。这可能会在 1、3、5、10 甚至 20 秒内发生。直到发生这种情况,它才会跳出运行循环,因此它不会注意到这个变量已经改变。IOW 您引用的 Apple 代码是不确定的。

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

于 2008-10-06T11:31:19.927 回答
2

我在尝试管理时遇到了类似的问题NSRunLoops。类参考页面上的讨论说:runMode:beforeDate:

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

我最好的猜测是,输入源NSRunLoop可能是由 OS X 本身附加到您的runMode:beforeDate:. 在您的情况下,发生这种情况需要“几秒钟到最多 10 秒钟”,此时runMode:beforeDate:将返回一个布尔值,while()将再次运行,它会检测到shouldKeepRunning已设置为NO,并且循环将终止。

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

于 2008-09-29T20:08:11.470 回答
1

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

有时我会为您的第一个示例找到解决方案:

BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]);
于 2013-08-14T01:48:16.603 回答