【问题标题】:Correct usage of secondary NSThread with NSRunLoop使用 NSRunLoop 正确使用辅助 NSThread
【发布时间】:2019-09-13 10:31:26
【问题描述】:

我有一个性能敏感的代码,实时处理视频播放的帧。我在这里有一些可以并行化的工作,因为它是一个性能敏感的代码,其中延迟是我决定使用NSThread 而不是GCD 的关键。

我需要什么:我需要NSThread,它将在某个时间段安排一些工作。线程完成工作后,它会进入休眠状态,直到新的工作到来。

不幸的是,互联网上没有太多关于NSThread 使用正确技术的信息,所以我根据我设法找到的信息位组装了我的例程。

您可以在上面找到整个工作流程:

1) 初始化我的NSThread。此代码仅按预期启动一次。

_myThread = [[NSThread alloc] initWithTarget:self selector:@selector(_backgroundMethod) object:nil];
 _myThread.threadPriority = 0.8;    //max priority is 1.0. Let's try at 0.8 and see how it performs
[_myThread start];

2)_backgroundMethod代码:

- (void)_backgroundMethod
{
    NSLog(@"Starting the thread...");
    [NSTimer scheduledTimerWithTimeInterval:FLT_MAX target:self selector:@selector(doNothing:) userInfo:nil repeats:YES];

    BOOL done = false;
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    do {
        [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    } while (!done);
}

- (void)doNothing:(NSTimer *)sender { }

3) 当线程有事情要处理时,我会进行下一次调用:

[self performSelector:@selector(_doSomeCalculation) onThread:_myThread withObject:nil waitUntilDone:NO];

调用下一个方法:

- (void) _doSomeCalculation
{
    //do some work here
}

所以我的问题是:

1) 当我初始化NSThread 时,我传递了一个选择器。该选择器的目的是什么?据我了解,这个选择器的唯一目的是控制线程的 RunLoop,我不应该在这里做任何计算。因此,我正在使用无限计时器让NSRunLoop 保持活力,而无需不断运行while 循环。这是正确的方法吗?

2) 如果我可以在 NSThread 初始化阶段传递的选择器中进行计算 - 我如何在不使用 performSelector 的情况下向 NSRunLoop 发出信号以执行一个循环?我认为我不应该使用performSelector 传递完全相同的方法,因为它会一团糟,对吧?

我已经阅读了 Apple 提供的大量信息,但所有这些信息几乎都是理论上的,而提供的那些代码示例让我更加困惑..

任何澄清将不胜感激。提前致谢!

编辑:

还有一个问题 - 如何为我的线程计算所需的 stackSize?有什么技术可以做到吗?

【问题讨论】:

  • 错误的决定。使用GCD。它旨在“杀死”NSThread。它有你需要的一切。你现在只是在重新发明 GCD。您可以直接使用GCD 或Objective-C 的包装器NSOperationQueue
  • 您好!以下是 Apple 在文章 Migrating Away from Threads - It is important to remember that queues are not a panacea for replacing threads. The asynchronous programming model offered by queues is appropriate in situations where latency is not an issue. Even though queues offer ways to configure the execution priority of tasks in the queue, higher execution priorities do not guarantee the execution of tasks at specific times. Therefore, threads are still a more appropriate choice in cases where you need minimal latency, such as in audio and video playback. 中对 NSThread 的评价
  • 所以我想指出,我决定尝试NSThread,因为在这种特殊情况下,GCD 存在某些问题——比如延迟会随着时间的推移而降低,并且不同设备上的性能不均衡。我需要更多地控制我的任务的执行方式并学习如何正确地完成它
  • 我认为苹果在这里的意思是你在线程中执行简单的后续工作:从输入中获取数据,处理它,将其放入输出。当您创建运行循环并执行选择器时,不是这样的调度机制。执行选择器也比直接调用函数需要更多时间,如果延迟如此重要,你应该考虑一下。
  • 我将不得不与@Cy-4AH 合作。要么使用 GCD 在单个队列上完成所有工作,要么使用 NSThread 而不为每个进程运行循环。看来你不止一个。在这两种情况下,当您需要在此线程中完成的工作太多而无法处理时,您都会产生延迟。打开的线程数量会增加,或者前一个任务还没有完成,新的任务将被安排,从而产生延迟。后者基本上是GCD应该做的。

标签: ios objective-c multithreading nsthread nsrunloop


【解决方案1】:

因为它是一个性能敏感的代码,延迟是关键,我决定使用 NSThread 而不是 GCD。

您通常不应该这样做,除非您对 GCD 有深入的了解并且确切地知道您要放弃什么。如果使用得当,GCD 是高度优化的,并且与操作系统非常紧密地集成在一起。手动使用 NSThread 尤其令人惊讶,但随后使用 performSelector 和运行循环在 ObjC 中进行工作。以这种方式调用 performSelector 会引入与 GCD serial 队列相同的未知延迟。如果线程已经很忙,那么您将对选择器进行排队,就像排队一个块一样(但您将添加objc_msgSend 的开销)。 GCD 并发队列会执行得更好。为了匹配 GCD,您需要实现一个适当的线程池(或至少添加取消)。做得好,对于特定的用例,这可能比 GCD 更好,但它必须做得很好,这很复杂。

正如Threading Programming Guide 所说:

注意:虽然对于线程之间的偶尔通信有好处,但您不应该使用 performSelector:onThread:withObject:waitUntilDone: 方法来进行时间紧迫或线程之间频繁的通信。

如果您想要线程之间的低延迟通信,您通常需要使用信号量,例如 NSConditionLock,而不是运行循环。

话虽如此,让我们来看看实际问题。

performSelector:onThread:...接口一般用于一次性操作。实现长时间运行的专用线程的更常见方法是继承NSThread 并覆盖main。像这样的东西(这是根据线程编程指南中的代码拼凑起来的,未经测试;多年来我已经在 GCD 中完成了所有高性能工作,所以我可能在这里搞砸了)。

#import "WorkerThread.h"

#define NO_DATA 1
#define HAS_DATA 2

@implementation WorkerThread
static NSConditionLock *_condLock;
static NSMutableArray *_queue;

+ (void)initialize
{
    if (self == [WorkerThread class]) {
        _condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
        _queue = [NSMutableArray new];
    }
}

- (void)main
{
    // Until cancelled, wait for data, and then process it.
    while (!self.cancelled)
    {
        [_condLock lockWhenCondition:HAS_DATA];
        id data = [_queue firstObject];
        [_queue removeObjectAtIndex:0];
        [_condLock unlockWithCondition:(_queue.count == 0 ? NO_DATA : HAS_DATA)];

        // Process data
    }
}

// Submit work to the queue
+ (void)submitWork:(id)data {
    [_condLock lock];
    [_queue addObject:data];
    [_condLock unlockWithCondition:HAS_DATA];
}

@end

你会产生一些线程,例如:

workers = @[[WorkerThread new], [WorkerThread new]];
for (worker in workers) {
    [worker start];
}

[WorkerThread submitWork: data];

你会关闭这样的线程:

for (worker in workers) {
    [worker cancel];
}

【讨论】:

  • 感谢您的澄清!我对GCD 的主要担心是它的内部机制可以自动调整QoS,我真的不知道如何影响它,除了从一开始就把所有工作都放在较低的QoS 上。我可以使用什么方法让GCD 对我的QoS 选择和线程工作量感到满意?
  • 您应该选择与您的意图相匹配的 QoS。如果这是影响 UI 的实时处理,那么您应该将这些工作项标记为.userInteractive。这恰好是最高的 QoS,但 QoS 不仅仅是优先级。您不应该尝试调整一些数字以使事情按顺序处理(如果您需要订单,请使用队列)。您应该根据他们的意图对其进行标记。
  • 如果您对使用 GCD 进行实时处理有任何疑问,我建议您就此提出一个新问题,详细说明您遇到的具体问题。如果这真的是实时工作,那么也值得讨论如何处理丢帧。 (如果您没有一种机制来放弃工作以达到最后期限,那么它们就不是真正的最后期限,也不是真正的“实时”。)
  • 如果我没记错的话,AVCaptureVideoDataOutput 会帮我丢帧。如果我没有跟上我的工作,我会看到captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) 被调用,因此我不会处理“迟到的帧”。还是你指的是别的东西?
  • 根据您的情况,这可能就足够了。但是,如果您已经将一堆东西发送到多个线程,您可能需要放弃超过其截止日期的处理,或者一旦开始丢弃,您可能永远赶不上。捕获连接需要知道您落后了,如果您将事情分拆到其他线程,它不会自动知道这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-04
  • 2011-01-02
  • 1970-01-01
  • 2021-12-31
  • 2014-07-17
  • 1970-01-01
相关资源
最近更新 更多