如何使用GCD有效地读取数千个小文件

时间:2014-05-12 00:01:55

标签: objective-c cocoa grand-central-dispatch

我想尽可能高效地从可能数千个文件中读取一些元数据数据(e.x。:EXIF数据),而不会影响用户体验。我很感兴趣,如果有人对如何使用常规GCD队列,dispatch_io频道或甚至其他实现进行最佳处理有任何想法。

选项#1:使用常规GCD队列。

这个非常简单我只能使用以下内容:

for (NSURL *URL in URLS) {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    // Read metadata information from file.
    CGImageSourceCopyProperties(...);
  });
}

我认为(并且经历过)这个实现的问题是,GCD不知道块中的操作是否与I / O相关,因此它将数十个块提交到全局队列进行处理,反过来使I / O饱和。系统最终会恢复,但如果我正在阅读数千或数万个文件,那么I / O会受到影响。

选项#2:使用dispatch_io

这个看起来像是一个很好的竞争者,但实际上我使用常规GCD队列时性能更差。那可能是我的实施。

dispatch_queue_t intakeQueue = dispatch_queue_create("someName"), NULL);

for (NSURL *URL in URLS) {    
  const char *path = URL.path.UTF8String;
  dispatch_io_t intakeChannel = dispatch_io_create_with_path(DISPATCH_IO_RANDOM, path, O_RDONLY, 0, intakeQueue, NULL);
  dispatch_io_set_high_water(intakeChannel, 256);
  dispatch_io_set_low_water(intakeChannel, 0);

  dispatch_io_handler_t readHandler = ^void(bool done, dispatch_data_t data, int error) {
    // Read metadata information from file.
    CGImageSourceCopyProperties(...);
    // Error stuff...
  };

  dispatch_io_read(intakeChannel, 0, 256, intakeQueue, readHandler);
}

在第二个选项中,我觉得我有点滥用dispatch_read。我对它读取的数据不感兴趣,我只是希望dispatch_io能够为我节省I / O. 256大小只是一个随机数,因此即使我从不使用它,也会读取一定数量的数据。

在第二个选项中,我有几次运行,系统工作“非常好”,但我也有一个实例,我的整个机器锁定(甚至光标),我不得不硬复位。在其他情况下(同样随机),应用程序只是退出堆栈跟踪,看起来像是几十个试图清理的dispatch_io调用。 (在所有这些情况下,我试图读取超过10,000张图像。)

(由于我自己没有打开任何文件描述符,而且GCD块现在是ARC友好的,我认为在dispatch_io_read完成之后我不需要做任何明确的清理工作,尽管可能我错了?)

解决方案?

我可以使用其他选项吗?我考虑使用NSOperationQueuemaxConcurrentOperationCount的低值手动限制请求,但这似乎是错误的,因为较新的MacPros可以清楚地处理比旧版本更多的I / O. -SSD,MacBook。

更新1

我想根据@ Ken-Thomases在下面提到的一些观点对选项#2做一点修改。在此尝试中,我试图通过在请求的总字节数之下设置dispatch_io标记来阻止high_water块退出。这个想法是读取处理程序将被调用,剩余的数据将被读取。

dispatch_queue_t intakeQueue = dispatch_queue_create("someName"), NULL);

for (NSURL *URL in URLS) {    
  const char *path = URL.path.UTF8String;
  dispatch_io_t intakeChannel = dispatch_io_create_with_path(DISPATCH_IO_RANDOM, path, O_RDONLY, 0, intakeQueue, NULL);
  dispatch_io_set_high_water(intakeChannel, 256);
  dispatch_io_set_low_water(intakeChannel, 0);
  __block BOOL didReadProperties = NO;

  dispatch_io_handler_t readHandler = ^void(bool done, dispatch_data_t data, int error) {
    // Read metadata information from file.
    if (didReadProperties == NO) {
        CGImageSourceCopyProperties(...);
        didReadProperties = YES;
    } else {
      // Maybe try and force close the channel here with dispatch_close?
     }        
  };

  dispatch_io_read(intakeChannel, 0, 512, intakeQueue, readHandler);
}

这似乎会减慢dispatch_io次调用的速度,但现在导致CGImageSourceCreateWithURL的调用在应用程序的其他部分(从未习惯过)失败。 (现在CGImageSourceCreateWithURL随机返回NULL,如果我不得不猜测,则表明它无法打开文件描述符,因为文件肯定存在于给定路径中。)

更新2

在尝试了其他六个想法之后,使用NSOperationQueue和调用addOperationWithBlock这样简单的实现效果与我能提出的其他任何内容一样有效。手动调整maxConcurrentOperationCount有一些影响,但远不及我想象的那么多。

显然,SSD和外部USB 3.0驱动器之间的性能差异非常大。虽然我可以在合理的时间内在SSD上迭代超过100,000张图像(甚至可以逃脱大约200,000张图像),但USB驱动器上的许多图像都无望。简单的数学:(读取*文件计数/驱动器速度所需的字节数)表明我无法真正获得我希望的用户体验。 (仪器似乎表明_CGImageSourceBindToPlugin每个文件的读数大约在40KB到1MB之间。)

2 个答案:

答案 0 :(得分:4)

现实情况是,一个现代化的多任务,多用户系统可以运行多种硬件配置,自动限制I / O绑定任务,这对系统来说几乎是不可能的。

你将不得不自己做限制。这可以通过NSOperationQueue,信号量或任何其他机制来完成。

通常情况下,我建议您尝试将I / O与任何计算分开,以便您可以序列化I / O(这将是所有系统中最普遍合理的性能),但这几乎是不可能的。使用高级API。事实上,尚不清楚CG * I / O API如何与dispatch_io_ *顾问API交互。

不是一个非常有用的答案。如果不了解更具体的案例,就很难更具体。我建议缓存可能是关键;为所有各种图像建立元数据数据库。当然,那么你有同步和验证问题。

答案 1 :(得分:4)

如果GCD提供了一种根据他们要对I / O进行I / O操作的磁盘设备来对任意块进行负载平衡的方法,那就不错了,但事实并非如此。您对调度I / O的使用最终与您的第一种方法没有太大区别。

Dispatch I / O代表您执行256字节的文件读取。但是,一旦读取了数据,即使您的数据处理块没有运行完成,它也可以允许继续读取另一个文件。所以,很快,一堆数据处理块就会同时排队,就像你的第一个解决方案一样。在某种程度上,CGImageSourceCopyProperties()中隐含的I / O与调度I / O竞争,因此可能会限制数据处理任务的提交,但可能还不够。

将调度I / O应用于此问题的明显/天真的方法是将每个整个图像文件读入数据对象,然后使用它来使用CGImageSourceCreateWithData()创建图像源。问题在于,当实际只需要复制属性时,它会读取整个图像文件。

您可以尝试使用使用CGImageSourceCreateIncremental()创建的增量图像源来改善此问题。您可以从文件中调度I / O读取图像数据的一些重要块(可能是设备块大小),将其连接到可变数据对象上,并使用CGImageSourceUpdateData()更新图像源。然后,使用CGImageSourceGetStatus()检查图像来源的状态。您将继续以这种方式读取数据,直到状态指示可以复制图像源属性。希望CGImageSourceCopyProperties()能够在图像完成之前成功,因此您不必阅读所有图像文件数据 - 也就是说,状态从kCGImageStatusReadingHeader转换为{{1}之后}。 (当然,kCGImageStatusIncomplete也表明它已经准备就绪。)

使用kCGImageStatusComplete和使用CGImageSourceUpdateDataProvider()创建的数据提供程序更新增量图像源可能更有效。然后,您将编写回调以使用调度数据函数。这样,您可以使用CGDataProviderCreateDirect()累积文件数据,而不需要复制缓冲区。

虽然它(可能不必要地)变得复杂,但可能做得更好。您可以使用dispatch_data_create_concat()创建直接数据提供者。然后使用CGDataProviderCreateDirect()从中创建非增量图像源。然后在该数据提供者上调用CGImageSourceCreateWithDataProvider()。在创建期间或可能直到您复制属性,图像源将向数据提供者询问数据。它会调用你的回调。此时,您没有要提供的任何数据,因此您必须失败(返回文件结尾)。但您可以使用该调用的性质来了解CGImageSourceCopyProperties()提供属性所需的文件部分。

然后,您可以使用调度I / O读取请求的数据。获得该数据后,您可以从数据提供程序创建新的图像源,然后重试。这次你提供你拥有的数据。 CGImageSource可能会要求更多数据,因此您需要重复此过程,直到您成功提供了复制属性所需的所有数据。

再一次,最好将所有请求舍入并对齐到整个设备块,并使用文件的第一个块来填充数据提供者,因为这肯定是必需的。


完全不同的方法是找出每个文件的物理设备。然后将用于将其图像属性复制的任务提交到专用于该设备的串行队列。每次识别新设备时,都要为其创建新的串行队列。但是,对于所有文件都在同一设备上的常见情况,这将简单地序列化操作(加上增加开销)。因此,正如您所提到的,可能是一个具有较小并发限制的操作队列,除了每个设备。我不认为这需要根据CPU速度甚至磁盘速度进行扩展,因为我怀疑复制图像属性的非I / O组件非常小。