【问题标题】:Run parallel code in an NSOperationQueue with an NSOperation使用 NSOperation 在 NSOperationQueue 中运行并行代码
【发布时间】:2013-06-04 15:36:39
【问题描述】:

我在这里遇到了一些问题。我正在开发一个读取文件并在 UITableView 中显示其内容的应用程序。我最近意识到文件可能会变得非常大,并且我需要对文件的实际读取进行异步编码。我的项目已经很大了,今天我设法将我已经拥有的代码包装在一个 NSOperation 中。

我所做的是我的解析器(打开和读取我的文件)现在在 NSOperation 中调用。就这样:

@implementation ReadPcapOperation

@synthesize parser =_parser;

- (id) initWithURL:(NSURL *)url linkedTo:(PacketFlowViewController *)packetController
{
    self = [super init];
    if (self) {
        _parser = [[PcapParser alloc] initWithURL:url linkedTo:packetController];
    }
    return self;
}

- (void)main {
    // a lengthy operation
    @autoreleasepool {
        if (self.isCancelled)
            return;

        [_parser read];

    }
}

@end

我在这里只给你实现,.h 文件中没有什么重要的。这个 NSOperation 子类在 NSOperation 中被调用:

_queue = [NSOperationQueue new];
_queue.name = @"File Parsing Queue";
_queue.maxConcurrentOperationCount = 1;
[_queue addOperation:_readFileOperation];

_readFileOperation 是上述 ReadPcapOperation 的一个实例。

现在,当我测试我的代码时,仍然没有区别,当我打开文件时,UI 仍然被阻止,而文件内容正在加载到我的 UITableView 中。我用这种情况进行了测试:

[NSThread isMainThread]

这个测试在 ReadPcapOperation 的 main 中返回 NO,这很好,正是我需要的。但是当我把它放在方法“read”中时,这个测试返回 YES,消息从 ReadPcapOperation 发送到 main 内的对象!所以我的整个代码仍在主线程上运行并阻塞了我的 UI。

我在这里错过了什么,伙计们?

如果您需要更多解释,请告诉我!

编辑:

奇怪的是:我将发布应该在后台线程中执行的部分代码。

- (void) read
{
    if ([NSThread isMainThread])
        NSLog(@"read: IT S MAIN THREAD");
    else
        NSLog(@"read: IT S NOT MAIN THREAD");

    [_fileStream open];
}

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
    switch(eventCode)
    {
        case NSStreamEventOpenCompleted:
        {
            //We read the pcap file header
            [self readGlobalHeader];
            [self readNextPacket];
            break;
        }
        case NSStreamEventHasBytesAvailable:
        {
            //We read all packets
            [self readNextPacket];
            break;
        }
        case NSStreamEventNone:
        {
            break;
        }
        case NSStreamEventHasSpaceAvailable:
        {
            break;
        }
        case NSStreamEventEndEncountered:
        {
            NSLog(@"End encountered !");
            [_fileStream close];
            [_fileStream removeFromRunLoop:[NSRunLoop currentRunLoop]
                              forMode:NSDefaultRunLoopMode];
            //_fileStream = nil;
            break;
        }
        case NSStreamEventErrorOccurred:
        {
            NSError *theError = [stream streamError];
            NSLog(@"Error %i stream event occured. Domain : %@.", theError.code, theError.domain);
            [stream close];
            break;
        }
    }
}

- (void) readGlobalHeader
{
    if ([NSThread isMainThread])
        NSLog(@"readGlobalHeader: IT S MAIN THREAD");
    else
        NSLog(@"readGlobalHeader: IT S NOT MAIN THREAD");
    int sizeOfGlobalHeader = 24;

在这里,您可以看到我直接从 NSOperation 调用的方法 read。那里的日志说:“不是主线程”。到目前为止,一切都很好。读取打开 NSInputStreamObject,然后委托将调用“handleEvent”,此时我读取字节。当我调用“readGlobalHeader”并读取文件的第一个字节时,日志是“MAIN THREAD”。它不应该在后台线程中吗?我真的迷路了!

可能需要注意的是,当我初始化流时,在调用“读取”之前,我用这行代码设置了它(我不确定这是不是原因):

[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

编辑 2: 这是断点之后的回溯。行为就像我上面描述的那样。 (没有足够的声誉来发布图片)

Breakpoint at the read method Breakpoint after the stream is open

【问题讨论】:

  • read 方法是做什么的,你在哪里记录它是否在主线程上?在返回 YES 的日志语句上设置断点并发布回溯。
  • read 是一种简单地将“open”发送到 NSInputStream 对象的方法。我使用 NSInputStream 来读取我的文件。流打开后,我会解析文件,逐字节读取之类的东西。我编辑了我的帖子,看看吧!

标签: ios objective-c uitableview nsoperation nsoperationqueue


【解决方案1】:

在很大程度上重申其他答案:将NSOperation 与流 API 一起使用是错误的。它们对于大量计算密集型任务最有用。文件或网络 i/o 之类的事情主要涉及等待和使用涉及完成块或委托和回调方法的异步 API。

我似乎记得NSOperation 文档并没有提及太多,但在使用异步 API 时它们通常毫无意义。 (除了Rory O'Bryan 提到的,当使用“并发”操作时。但是,对于那些你没有利用操作队列的后台线程特性的那些,只有它们的操作管理和依赖关系,即如果你只有一个操作就没用)

发生的事情是read 方法中的[_fileStream open] 正在操作队列中运行,然后它很快返回,然后操作完成。 stream:handleEvent: 委托回调不在open 内调用,而是在它返回后的一段时间内调用。它们在主线程上的调用与您的代码未使用 NSOperation 时没有区别。

morningstar 的回答不同,我假设您确实需要坚持使用流 API(如果不是,那么他的回答是有效的 - 转换为同步调用,您的操作应该按预期工作)。

我说你应该恢复不使用NSOperation - 它没有给你买任何东西。如果你喜欢 _fileStream 业务被封装在操作对象中的方式,请将你的操作对象转换成一个普通的NSObject 子类,然后调用它的read 方法。

查看您的 readGlobalHeaderreadNextPacket 方法。他们可能正在进行同步调用,这肯定是阻塞主线程的原因。如果确实如此,您有几个选择:

  • 遵循Ramy Al Zuhouri 的建议,除了使用dispatch_async 来包装对readNextPacket 等方法的调用。从理论上讲,您可以按照他的明确示例,也可以对[_fileStream open] 执行此操作,但只有在您测量到此调用需要很长时间才能返回时,我才会这样做。 GCD 是一种让这些方法在后台线程中运行的简单方法,只需阅读正确设置队列的内容即可。

  • 就像Rory O'Bryan 建议的那样,更改流调度,以便在您创建的线程中调用您的委托方法。创建线程和运行循环可能很棘手,但this example 似乎展示了一种简单的方法,尽管可能不是最好的。您至少必须添加一些东西来停止线程。

    我认为最好的顺序是:

    1. th = [[NSThread alloc] init...]
    2. [_fileStream scheduleInRunLoop:<thread's-runloop> ...]
    3. [th start]
    4. 线程方法只调用一次[[NSRunLoop currentRunLoop] run]

    然后当流关闭时,它从runloop中取消调度,run方法返回,所以线程的方法返回,线程结束。请注意,我可能错了,毕竟您可能需要将[[NSRunLoop currentRunLoop] run] 放在一个循环中。

  • 转换 readNextPacket 等以使用异步 API。我想说这可能是最有意义的,除非您在这些方法中进行大量计算,而不仅仅是网络 i/o。

【讨论】:

  • 很好的重申!这个答案应该重新阅读,直到理解为止。 ;)
【解决方案2】:

我的猜测是问题在于您如何设置流。行:

[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

在主线程上被调用,所以[NSRunLoop currentRunLoop] 是主线程的运行循环,因此你的委托回调在主线程上被调用。 NSStreamDelegate 协议 stream:handleEvent: 方法的文档指出:

只有当 theStream 被安排在一个 运行循环。消息在流对象的线程上发送。这 委托应检查 streamEvent 以确定适当的 它应该采取的行动。

我猜测在主线程的 runloop 上进行调度意味着 stream objects 线程 就是主线程。 (?)

(在尝试回答这个问题时,我在文档中阅读了一些关于运行循环和线程的信息,但是否是这种情况并不完全清楚。如果@bbum 跟进,他肯定可以提供明确的答案)。

假设这是问题所在,那么您可以使用主线程简单地接收事件,然后可能通过使用另一个 NSOperation 将工作传递给另一个线程。或者您可能需要创建一个特定的线程和运行循环来安排流。

我读到了NSURLConnection 的类似情况,它在接收委托回调时也使用运行循环。在这种情况下很难找到设置和管理运行循环的示例,但是最近我查看了AFNetworking 的代码,发现这是他们管理NSURLConnection 处理的方式。我认为类似的方法适用于流,因此您可能会发现这是一个有用的指南。

当然,与NSURLConnection 相比,对于流使用 runloop 的例子可能还有更好的例子,所以不妨先寻找那些。

编辑: 此外,您没有提及将您的操作配置为 concurrent 操作的任何内容。除非我在这里遗漏了什么,否则如果委托回调是异步的,例如 NSURLConnections,那么您是否有并发操作并且需要这样设置它?

【讨论】:

    【解决方案3】:

    发生这种情况是因为 stream:handleEvent: 作为委托方法,在主线程上被调用。我建议改变你的设计,不要使用 NSOperationQueue,而是让像 read 这样的方法在另一个线程上执行操作。

    在您的情况下,GCD 非常适合。所以我建议使用 *dispatch_queue_t* ivar 来执行每个需要异步调用的方法。在你的情况下 read 会改变这种方式:

    - (void) read
    {
        dispatch_async(self.queue, ^
        {
            if ([NSThread isMainThread])
                NSLog(@"read: IT S MAIN THREAD");
            else
                NSLog(@"read: IT S NOT MAIN THREAD");
    
            [_fileStream open];
        });
    }
    

    PS:记住需要创建队列。

    【讨论】:

    • 好的,我明白了。那么我有什么选择呢?我不熟悉并发和iOS。 NSThread 会完成这项工作吗?
    • 是的,会的。但在你的情况下,由于 read 执行几行代码并且应用程序很简单,我建议使用 GCD,并阅读这篇关于 GCD 的文章:cocoasamurai.blogspot.it/2009/09/…
    【解决方案4】:

    由于您的文件读取已经在后台线程中运行,您不需要像 NSStream 这样的异步方法。尝试只使用 NSFileHandle 和 readDataOfLength:

    我不知道你的文件格式。如果您可以在读取记录之前确定它的大小(恒定宽度,或根据您目前所阅读的内容可预测),那么您可以使用正确的记录大小调用readDataOfLength:。当它返回时,您将获得记录并将其添加到表格视图中。如果您无法预测记录的大小,则只需调用readDataOfLength:1024 并解析结果即可。当您找到记录的结尾时,添加它。如果在找到完整记录之前数据已用完,请再次调用readDataOfLength:1024 并从结果的开头继续读取。

    无论哪种方式,对readDataOfLength: 的某些调用都会阻塞,但这只会阻塞您的后台线程,无论如何它没有任何其他事情可做。保持一个被阻塞的后台线程并不太昂贵。

    【讨论】:

      【解决方案5】:

      感谢大家的回答,他们非常有帮助。我解决了我的问题,方法如下:

      首先我的流委托方法仍然在主线程中被调用,我不知道为什么。这是因为我在我的 NSOperation 类的 init 方法中初始化了流,而不是在 main 中。因此,当我调用[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 时,它正在主线程循环中进行调度。我通过在 NSOperation main 中初始化 _filestream 解决了这个问题,它在后台线程中正确运行。

      然后我意识到后台线程实际上是在调用委托的方法之前完成并销毁的。在这一点上,我完全改变了我做后台处理的方式。正如你们中的一些人建议的那样,我只是使用了带有 dispatch_async 的 GCD。这实际上是我使用它的地方:

      - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
          switch(eventCode)
          {
              case NSStreamEventOpenCompleted:
              {
                  //We read the pcap file header
                  dispatch_async(myqueue, ^
                  {
                           [self readGlobalHeader];
                           [self readNextPacket];
                   });
                  break;
              }
              case NSStreamEventHasBytesAvailable:
              {
                  //We read all packets
                  [self readNextPacket];
                  break;
              }
      

      而且效果很好。我正在使用发送到主队列的方法刷新我的 UI。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-11-14
        相关资源
        最近更新 更多