NSRunLoop的runMode:beforeDate: - 设置“beforeDate”的正确方法

时间:2012-11-09 18:51:11

标签: ios multithreading cocoa concurrency nsrunloop

我对NSRunLoop的runMode:beforeDate方法的正确用法存有疑问。

我有一个辅助后台线程,可以在收到邮件时处理它们。

基本上,我有需要在后台线程上执行的进程密集型逻辑。

所以,我有2个对象,ObjectAAnotherObjectB

ObjectA初始化AnotherObjectB并告诉AnotherObjectB开始做这件事。 AnotherObjectB异步工作,因此ObjectA充当AnotherObjectB的委托。现在,需要在委托消息中执行的代码需要在后台线程上完成。所以,对于ObjectA,我创建了一个NSRunLoop,并做了类似的事情来设置运行循环:

do {
 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (aCondition);

aCondition设置在“完成委托消息”的某处。

我收到了所有委托消息,并且正在后台线程中处理它们。

我的问题是:这是正确的做法吗?

我之所以这样问是因为[NSDate distantFuture]是一个跨越几个世纪的日期。所以基本上,runLoop在“remoteFuture”之前不会超时 - 我肯定不会在那之前使用我的Mac或这个版本的iOS。 > _<

但是,我不希望运行循环运行那么久。我希望在调用上一个委托消息后立即完成运行循环,以便它可以正确退出。

另外,我知道我可以用较短的间隔设置重复计时器,但这不是最有效的方式,因为它类似于轮询。相反,我希望线程仅在委托消息到达时工作,并在没有消息时休眠。那么,我采取的方法是正确的方法,还是有其他方法可以做到这一点。我阅读了文档和指南,并根据我从阅读中理解的内容进行了设置。

然而,如果不完全确定,最好向这个令人敬畏的社区征求意见和确认。

所以,提前感谢您的帮助!

干杯!

2 个答案:

答案 0 :(得分:2)

代码为in the docs

  

如果希望运行循环终止,则不应使用此方法。相反,使用其他运行方法之一,并在循环中检查您自己的其他任意条件。一个简单的例子是:

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

其中shouldKeepRunning在程序中的其他位置设置为NO

在你的最后一条“消息”之后,取消设置shouldKeepRunning(与运行循环在同一个线程上!),它应该完成。这里的关键思想是你需要向运行循环发送一个事件,以便它知道停止。

(另请注意,NSRunLoop不是线程安全的;我认为您应该使用-[NSObject performSelector:onThread:...]。)

或者,如果它适用于您的目的,请使用后台调度队列/ NOperationQueue(但请注意,执行此操作的代码不应触及运行循环;例如从调度队列/ NSOperationQueue工作线程启动NSURLConnection等操作将可能会导致问题)。

答案 1 :(得分:0)

<块引用>

我问这个的原因是因为 [NSDate distanceFuture] 是一个跨越几个世纪的日期。

方法 runMode:beforeDate:

  • 如果 NO 上没有安排任何来源,请立即返回 RunLoop

  • 每当处理完事件时返回 YES

  • 到达 YES 时返回 limitDate

因此,即使 limitDate 非常高,它也会在每个处理完的事件后返回,直到 limitDate 被命中它才会继续运行。如果没有处理任何事件,它只会等待那么长时间。因此,limitDate 就像一个超时,之后该方法将放弃等待事件发生。但是如果你想连续处理多个事件,你必须一遍又一遍地调用这个方法,因此循环。

考虑从网络套接字获取超时数据包。当数据包到达或达到超时时,fetch 调用返回。但是如果要处理下一个数据包,则必须再次调用fetch方法。

不幸的是,以下代码非常糟糕,原因有两个:

// BAD CODE! DON'T USE!
NSDate * distFuture = NSDate.distantFuture;
NSRunLoop * runLoop = NSRunLoop.currentRunLoop;
while (keepRunning) {
    [runLoop runMode:NSDefaultRunLoopMode beforDate:distFuture];
}
  1. 如果还没有在 RunLoopSource 上调度 RunLoop,它将浪费 100% 的 CPU 时间,因为该方法将立即返回以再次被调用,并且速度与CPU 可以这样做。

  2. AutoreleasePool 永远不会更新。自动释放的对象(甚至 ARC 也会这样做)被添加到当前池中,但永远不会被释放,因为池永远不会被清除,所以只要这个循环正在运行,内存消耗就会增加。多少取决于您的 RunLoopSources 实际在做什么以及他们如何做。

更好的版本是:

// USE THIS INSTEAD
NSDate * distFuture = NSDate.distantFuture;
NSRunLoop * runLoop = NSRunLoop.currentRunLoop;
while (keepRunning) @autoreleasepool {
    BOOL didRun = [runLoop runMode:NSDefaultRunLoopMode beforDate:distFuture];
    if (!didRun) usleep(1000);
}

它解决了这两个问题:

  • 循环第一次运行时会创建一个 AutoreleasePool,每次运行后都会清除它,因此内存消耗不会随着时间的推移而增加。

  • 如果 RunLoop 根本没有真正运行,当前线程会休眠一毫秒,然后再试一次。这样 CPU 负载会非常低,因为没有设置 RunLoopSource,此代码每毫秒只运行一次。

要可靠地终止循环,您需要做两件事:

  1. keepRunning 设置为 NO。请注意,您必须将 keepRunning 声明为 volatile!如果您不这样做,编译器可能会优化检查并将您的循环变成无限循环,因为它在当前执行上下文中看不到任何代码会改变变量,并且它无法知道其他地方的其他代码(也许在另一个线程上)可能会在后台更改它。这就是为什么在这些情况下通常需要内存屏障(锁、互斥锁、信号量或原子操作),因为编译器不会跨这些屏障进行优化。但是,在这种简单的情况下,使用 volatile 就足够了,因为 BOOL 在 Obj-C 中始终是原子的,而 volatile 告诉编译器“始终检查此变量的值作为它可能会在您背后发生变化,而您在编译时没有看到该变化”。

  2. 如果变量是从另一个线程而不是在事件处理程序中更改的,则您的 RunLoop 线程可能在 runMode:beforeDate: 调用中休眠,等待 RunLoopSource发生的事件可能需要任何时间或永远不会发生。要强制此调用立即返回,只需在更改变量后安排一个事件。这可以通过 performSelector:onThread:withObject:waitUntilDone: 完成,如下所示。执行此选择器算作一个 RunLoop 事件,该方法将在选择器被调用后返回,看到变量已更改并跳出循环。

volatile BOOL keepRunning;

- (void)wakeMeUpBeforeYouGoGo {
    // Jitterbug
}

// ... In a Galaxy Far, Far Away ...
    keepRunning = NO;
    [self performSelector:@selector(wakeMeUpBeforeYouGoGo) 
        onThread:runLoopThread withObject:nil waitUntilDone:NO];