2

我对 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 直到“distantFuture”才会超时——在那之前我绝对不会使用我的 Mac 或这个版本的 iOS。>_<

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

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

但是,如果不能完全确定,最好向这个很棒的社区征求意见和确认。

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

干杯!

4

2 回答 2

2

代码在文档中

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

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

whereshouldKeepRunning设置为NO程序中的其他位置。

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

(还要注意 NSRunLoop 不是线程安全的;我认为你应该使用-[NSObject performSelector:onThread:...].)

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

于 2012-11-09T20:54:51.017 回答
1

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

该方法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尚未在 上安排no 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);
}

它解决了两个问题:

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

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

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

  1. 设置keepRunningNO。请注意,您必须声明keepRunningvolatile!如果您不这样做,编译器可能会优化检查并将您的循环变成无限循环,因为它在当前执行上下文中看不到任何会更改变量的代码,并且它无法知道其他地方的其他代码(也许在另一个线程上)可能会在后台更改它。这就是为什么您通常需要在这些情况下使用内存屏障(锁、互斥体、信号量或原子操作),因为编译器不会跨这些屏障进行优化。但是,在那种简单的情况下,使用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];

于 2021-04-30T00:58:18.027 回答