在iOS上管理CPU密集型线程

6
我是一名有经验的C/C++程序员,正在学习iPhone上的Objective C。我已经进行了大量搜索,但没有找到一个令人满意的答案,这应该是一个常见问题;如果此问题已经有答案,请多包涵,我希望得到指引。
我的应用非常CPU密集型。UI有一个简单的显示屏,显示进度和启动/停止按钮。在确保更新显示屏和响应按钮的同时,如何分配最多的CPU周期来完成工作是最好的方法?我已经读到你不应该在主线程中执行任务,但除此之外我没有找到更多的建议。因此,我已经在NSOperation队列中实现了我的工作。我还将屏幕刷新放在了它自己的队列中。我也使用了NSThread sleepForTimeInterval函数。我尝试了不同的睡眠时间,从.001秒到1秒(例如[NSThread sleepForTimeIntervals .1])。尽管如此,屏幕显示最多只能达到缓慢状态(10秒钟),按下停止按钮后,按钮会高亮显示,但需要再等待10秒钟才能响应。
1.) NSOperation队列是一个合理的选择吗?如果不是,还有什么其他的选择? 2.) 如何最小化休眠?(显然,我希望能让工作获得尽可能多的周期,而我不确定我的休眠是否对更新UI有什么作用。) 3.) 有没有更好的技术来保持UI处于最新状态?例如,是否可以使用NSTimer或其他方法发送消息到UI,告诉它更新和/或检查按钮的状态?
谢谢您的支持。
4个回答

5

1.) NSOperation Queues是一个合理的选择吗?如果不是,还有什么其他选择?

NSOperationQueue听起来是个不错的选择。

当然,你还有其他选择:pthreads、libdispatch(也称为GCD)、基于pthreads的C++线程库等等。如果你不需要生成太多操作,那就只取决于你喜欢哪种模型了。

2.) 如何最小化睡眠时间?(显然我希望工作能得到尽可能多的周期/合理的周期,而且我不确定我的睡眠是否对所有UI更新都起作用。)

不要睡觉=) 你可以为你的UI元素使用定时器或显式回调或通知来通知依赖项。如果依赖项执行UI更新,则你很可能将消息添加到主线程的消息队列中。

3.) 有没有更好的技术来保持UI更新?例如,我可以使用NSTimer或其他方法向UI发送消息,告诉它更新和/或检查按钮的状态吗?

这真的取决于你在做什么。如果你只想更新进度条,则可以从辅助线程中写入值,并从主线程中读取该值。然后,在主运行循环上使用定时器定期向你的对象发送消息,以更新其显示(基于当前值)。对于像未分段进度指示器这样的东西,这可能很好。

另一种选择更适用于事件或阶段:它将涉及从辅助线程发布更新(例如通知或委托的回调),随着进度的推移而进行更新(有关#2的更多信息)。

更新

我不确定这在iOS模型中是否合适,但听起来是可以的。

是的,没问题 - 有许多方法可供选择。哪个“最好”取决于上下文。

我的当前理解是在一个线程中启动UI(不是主线程!)

你实际上并没有显式地启动UI;主线程(通常)由将事件和消息推送到主线程驱动。主线程使用运行循环,并在每次运行循环迭代时处理队列中的消息/事件。你还可以在未来安排这些消息(稍后会详细介绍)。话虽如此,所有针对UIKit和AppKit(如果你针对OSX)对象的消息都应该在主线程上执行(作为一般化的规则,你最终会发现有例外情况)。如果你有一个完全与消息传递UIKit对象的方法分离的具体实现,并且该程序是线程安全的,那么你实际上可以从任何线程执行这些消息,因为它不会影响UIKit实现的状态。最简单的例子:

@interface MONView : UIView
@end

@implementation MONView
// ...
- (NSString *)iconImageName { return @"tortoise.png"; } // pure and threadsafe
@end

启动我的工作线程,使用定时器生成信号通知UI查看进度值并适当更新进度条。针对这个特定应用程序,你的倒数第二段已经足够了,我现在不需要进行最后一段的操作。谢谢。 要实现这个功能,您可以使用类似以下的方法:
@interface MONView : UIView
{
    NSTimer * timer;
    MONAsyncWorker * worker; // << this would be your NSOperation subclass, if you use NSOperation.
}

@end

@implementation MONView

// callback for the operation 'worker' when it completes or is cancelled.
- (void)workerWillExit
{
    assert([NSThread isMainThread]); // call on main

    // end recurring updates
    [self.timer invalidate];
    self.timer = nil;

    // grab what we need from the worker
    self.worker = nil;
    // update ui
}

// timer callback
- (void)timerUpdateCallback
{
    assert([NSThread isMainThread]); // call on main
    assert(self.worker);

    double progress = self.worker.progress;

    [self updateProgressBar:progress];
}

// controller entry to initiate an operation
- (void)beginDownload:(NSURL *)url
{
    assert([NSThread isMainThread]); // call on main
    assert(nil == worker); // call only once in view's lifetime

    // create worker
    worker = [[MONAsyncWorker alloc] initWithURL:url];
    [self.operationQueue addOperation:worker];

    // configure timer
    const NSTimeInterval displayUpdateFrequencyInSeconds = 0.200;
    timer = [[NSTimer scheduledTimerWithTimeInterval:displayUpdateFrequencyInSeconds target:self selector:@selector(timerUpdateCallback) userInfo:nil repeats:YES] retain];
}

@end

请注意,这只是一个非常基础的演示。通常将计时器、更新处理和操作放在视图控制器中而不是视图中更为常见。

我习惯按照你在最后两段描述的方式进行操作。我不确定这是否适用于iOS模型,但听起来是可以的。我的当前理解是在一个线程(而非主线程)中启动UI,启动工作线程,使用定时器生成信号以便UI查看进度值并相应地更新进度条。对于这个特定应用程序,你倒数第二段的描述已经足够了,我不需要采取最后一段的措施(至少目前不需要)。谢谢。 - user938797

3

您是否在主线程上进行UI更新?这非常重要,因为UIKit不是线程安全的,从辅助线程使用它可能会导致行为迟缓(或崩溃)。您通常不需要在后台线程/队列中使用sleep使UI保持响应(除非您的UI本身非常CPU密集型,但这似乎并非本例情况)。

您可以检查任何更新UI的方法是否在主线程上运行,例如:

NSAssert([NSThread isMainThread], @"UI update not running on main thread");

使用Grand Central Dispatch是一种简单且轻量级的方法,可以将UI更新与主线程同步:

dispatch_async(dispatch_get_main_queue(), ^ { 
    //do your UI updates here... 
});

感谢Assert!请为我澄清一些事情。是期望我在主线程上运行我的UI,然后工作线程不在主线程上...还是说我也应该将UI保持在非主线程上?我理解你的评论是UI应该在主线程上,但我想确保一下。 - user938797
你必须从主线程更新用户界面。请注意,NSOperationQueue 中的操作通常不在主线程上执行,因此,如果您正在操作内更新用户界面,则必须显式地在主线程/队列上执行该操作(参见我的第二个代码片段)。 - omz

2

以下是您提出问题的答案:

1) 由于您是一位经验丰富的C程序员,因此使用Grand Central Dispatch(GCD)这种基于C的并发API会让您感到非常舒适。

2) 使用GCD时,您根本不需要休眠。只需将需要执行的工作异步地调度到队列中,使用最高优先级(DISPATCH_QUEUE_PRIORITY_HIGH)即可。

3) 当您需要更新UI时,只需在同一块中使用dispatch_get_main_queue()将UI更新分派到主队列中进行处理。

请查看相关的GCD文档here


是的,我习惯使用线程。我知道NSOperation是建立在GCD之上的,只是它的一个简单入口。但如果有更合适的模型,我也很乐意使用。我已经查看了GCD文档(感谢指引),我可以自己进行调度。#2和#3是我熟悉的模型(来自Windows),我很愿意采用这种方式。谢谢。 - user938797

1

我会有一个模型对象来执行CPU任务,它有一个委托回调函数来处理输出变化,还有一个视图控制器。在 viewDidLoad 中,您将视图控制器设置为模型的委托。因此,模型可以使用线程,并在计算数据已更新时在主队列上发送消息。除非您的情况特别复杂,否则只需使用Grand Central Dispatch并将密集任务分派到另一个线程即可。

当然,您不应该在任何地方调用sleepForTimeInterval来实现您想要的效果。


非常好,这是我将使用的模型(其他答案也加强了这一点)。感谢大家的帮助! - user938797

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接