我对NSRunLoop的runMode:beforeDate
方法的正确用法存有疑问。
我有一个辅助后台线程,可以在收到邮件时处理它们。
基本上,我有需要在后台线程上执行的进程密集型逻辑。
所以,我有2个对象,ObjectA
和AnotherObjectB
。
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。 > _<
但是,我不希望运行循环运行那么久。我希望在调用上一个委托消息后立即完成运行循环,以便它可以正确退出。
另外,我知道我可以用较短的间隔设置重复计时器,但这不是最有效的方式,因为它类似于轮询。相反,我希望线程仅在委托消息到达时工作,并在没有消息时休眠。那么,我采取的方法是正确的方法,还是有其他方法可以做到这一点。我阅读了文档和指南,并根据我从阅读中理解的内容进行了设置。
然而,如果不完全确定,最好向这个令人敬畏的社区征求意见和确认。
所以,提前感谢您的帮助!
干杯!
答案 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];
}
如果还没有在 RunLoopSource
上调度 RunLoop
,它将浪费 100% 的 CPU 时间,因为该方法将立即返回以再次被调用,并且速度与CPU 可以这样做。
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
,此代码每毫秒只运行一次。
要可靠地终止循环,您需要做两件事:
将 keepRunning
设置为 NO
。请注意,您必须将 keepRunning
声明为 volatile
!如果您不这样做,编译器可能会优化检查并将您的循环变成无限循环,因为它在当前执行上下文中看不到任何代码会改变变量,并且它无法知道其他地方的其他代码(也许在另一个线程上)可能会在后台更改它。这就是为什么在这些情况下通常需要内存屏障(锁、互斥锁、信号量或原子操作),因为编译器不会跨这些屏障进行优化。但是,在这种简单的情况下,使用 volatile
就足够了,因为 BOOL
在 Obj-C 中始终是原子的,而 volatile
告诉编译器“始终检查此变量的值作为它可能会在您背后发生变化,而您在编译时没有看到该变化”。
如果变量是从另一个线程而不是在事件处理程序中更改的,则您的 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];