关于C#:使NSRunLoop等待设置标志的最佳方法是什么?

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]]);

我一直在使用它,它是有效的,但在调查性能问题时,我把它追踪到了这段代码。我使用几乎完全相同的代码(只是标记的名称不同),如果我在设置标记后(在另一种方法中)在行上放置一个NSLog,然后在while()之后放置一行,则两条日志语句之间会有几秒钟的随机等待。

在速度较慢或更快的机器上,延迟似乎没有什么不同,但在不同的运行中,延迟至少为几秒到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];
}

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

-威尔


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


在代码处,当前线程将每隔0.1秒检查变量是否已更改。在Apple代码示例中,更改变量不会有任何效果。runloop将一直运行,直到它处理某个事件。如果webviewisloading的值发生了变化,则不会自动生成任何事件,因此它将保持在循环中,为什么它会中断?它将停留在那里,直到它得到一些其他的事件处理,然后它将爆发出来。这可能在1、3、5、10甚至20秒内发生。在这种情况发生之前,它不会脱离runloop,因此不会注意到这个变量已经改变。你引用的苹果代码是不确定的。只有当webviewisloading的值更改也创建了一个导致runloop唤醒的事件时,此示例才会起作用,而事实似乎并非如此(或至少并非总是如此)。

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


我在管理NSRunLoops时也遇到过类似的问题。在课堂参考资料页面上对runMode:beforeDate:的讨论指出:

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.

我的最佳猜测是,一个输入源连接到您的NSRunLoop,可能是由OS X本身,并且runMode:beforeDate:正在阻塞,直到该输入源处理或删除某些输入。在您的情况下,这需要"几秒到10秒"才能发生,此时,runMode:beforeDate:将返回布尔值,while()将再次运行,它将检测到shouldKeepRunning已设置为NO,循环将终止。

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


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

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

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