【问题标题】:How do you cancel an NSArray sort that is in progress?如何取消正在进行的 NSArray 排序?
【发布时间】:2015-03-06 07:56:47
【问题描述】:

NSArrayNSMutableArray 分别提供了多种使用sortedArrayUsing...sortUsing... 方法对它们进行排序的方法,但是这些方法似乎都没有提供在排序开始后终止排序的方法。

对于相对较小的数组,或者比较逻辑很简单的时候,这可能没什么大不了的,但是对于较大的数组或者比较逻辑不简单的时候,我希望能够取消一个已经在进行中的排序.

简单的用例示例

根据用户的模糊搜索字符串对一组匹配的结果进行排序。当用户在搜索字段中键入时,结果会在后台线程上获取并在呈现给用户之前进行排序。如果在用户更改搜索字符串之前提取和排序操作没有完成,那么它应该被取消并开始一个新的提取和排序操作。问题是,如果 fetch-and-sort 操作已经到了排序阶段并调用了上面的NSArray 排序方法之一,那么就无法取消它。相反,下一个 fetch-and-sort 操作将等待现在陈旧的排序操作完成。

到目前为止,我已经提出了两种可能的解决方案,但似乎都不是那么优雅。

尝试的解决方案 #1

允许在任何陈旧的提取和排序操作完成之前启动较新的提取和排序操作。我只是使用一些内部状态来跟踪哪个是最新操作,并且随着操作完成,如果它们不是主要操作,那么它们的结果将被丢弃。

这是可行的,但它会很快导致多个未完成的排序操作同时运行,无论它们是否需要。这可以通过限制最大并发操作数来缓解,但我只是添加了一个任意限制。待处理的陈旧操作可以在它们执行之前被取消,但我仍然会遇到不需要排序工作的情况。

尝试的解决方案 #2

滚动我自己的快速排序或合并排序实现并将isCancelled 标志添加到这些例程中,以便它们可以快速展开和终止。这是有效的,并且工作得相当好,但是当不需要取消排序操作时,运行时间比使用 NSArray 方法之一慢约 15-20%。我想,部分原因是调用 objectAtIndexexchangeObjectAtIndex 等方法的开销,我假设内部排序例程可以绕过,具体取决于 NSArray 在内部存储相关对象的方式。

在 2015 年针对 AppKitNSArray 之类的东西推出我自己的排序实现也感觉错误

半尝试的解决方案

  • 保留一个先前排序的数组并重新使用它进行过滤:这对于我试图这样做的讨论并不有效,假设我必须排序的数组是始终未排序,并且与先前排序的数组没有关系。

  • NSArray 移回 C 样式数组:这工作得很好,性能也很好,但我只能用 ARC 玩一堆游戏,而且它的复杂性总体实现要高得多,因为归根结底,我总是在处理NSObjects。在 NSArray 和 C 样式数组之间返回和值的成本也非零。

总结

所以,所有这些都回到最初的问题:“如何取消正在进行的NSArray 排序方法?”

技术说明

对于那些好奇为什么这是一个问题开始的人,我正在尝试使用像 localizedStandardCompare 这样的比较方法对 500,000 到 1,000,000 个字符串之间的某个位置进行排序,这比直接的 NSString 比较慢得多。与排序的总时间相比,各种sortUsing... 方法之间的运行时间差异相对来说微不足道。

【问题讨论】:

  • 题外话:如果你有一个包含 1.000.000 个对象的大数组,如果你想对其进行排序,不要使用 nsarray,使用 ccarray github.com/manucorporat/CCArray 它比nsarray
  • 我认为编写自己的版本是唯一的方法。为了提高性能,也许您可​​以在一个类别中实现它并通过访问类变量来获得一些优势(不是最干净的 OO 解决方案,因为变量没有记录)。
  • 我认为这不可能,NSObject 提供了一种延迟取消请求的方法。此外,由于排序方法是同步运行的,因此您只能在它们完成后调用取消操作。在我看来,你应该使用枚举块创建你自己的实现或包装在 NSOperation 中。
  • 分块排序,然后合并。您可以在操作之间取消。

标签: ios objective-c macos cocoa appkit


【解决方案1】:

从你结束的地方开始:

所以,所有这些都回到最初的问题:“如何取消正在进行的 NSArray 排序方法?”

你没有。不支持取消,您想出的任何东西都一定是脆弱的。

回到你所做的事情:

滚动我自己的快速排序或合并排序实现,并为这些例程添加一个 isCancelled 标志,以便它们可以快速展开和终止。这是有效的,而且工作得相当好,但是当不需要取消排序操作时,运行时间比使用 NSArray 方法之一慢约 15-20%。

在这种情况下,这是要走的路,你只需要解决这个减速问题......

您可能是对的,减速的部分原因可能是需要调用索引和交换元素的方法。您是否尝试过将 C 函数指针缓存到您需要的常用方法?如果在排序开始时您获得指向objectAtIndex: 等的直接C 函数指针。使用 Objective-C 运行时函数class_getMethodImplementation(),您可以将所有对方法查找的调用替换为简单的间接调用。

如果这样的操作失败,那么可能会再次查看 C 数组。由于NSArray 是免费桥接到CFArrayRef,您可以使用CFArrayGetValues 将元素复制到malloc'ed C 数组中,对其进行排序,然后使用CFArrayCreate 回到@987654328 @。如果您小心并且不改变您正在排序的数组,因为元素将在原始数组中它们已经被保留并且创建新数组将再次保留它们,您可能什么都不做来处理内存管理。对 C 数组进行排序会更快,但提取和创建将是排序之上的 O(N) 操作。

HTH

【讨论】:

    【解决方案2】:

    经过几天的测试,我选择了一个自定义的就地合并排序实现,它接受一个布尔标志来触发取消。

    一些有兴趣的人的后续要点:

    • 我的合并排序实现的原始性能仍然落后于NSArray sortUsingComparator 方法的原始性能。 Instruments 表明 NSArray 也在使用合并排序,因此我怀疑性能差异可归因于 Apple 比我想出的更优化的实现以及直接访问 NSArray's 内部的能力。 NSArray's 实现使用 localizedStandardCompare 对 1,000,000 个字符串进行排序大约需要 28 秒,而我的实现需要 31.5 秒。 (MacBook Air 2013)

    • NSMutableArray 转换为对象的C 数组并没有产生足够的性能改进来保证增加的复杂性。排序时间仅减少了 0.5 - 1.0 秒。很高兴,但与在localizedStandardCompare 中花费的时间相比仍然相形见绌。对于更小尺寸(100,000)的输入数组,速度差异几乎可以忽略不计。这让我感到惊讶,但 Instruments 表明,与排序操作本身相比,使用 NSMutableArray 的所有“开销”主要是噪音。

    • 并行化合并功能并通过GCD 分出任务,显着改善了 6.0 - 7.0 秒,将排序的总时间减少到少于NSArray sortUsingComparator 所花费的时间。根据输入数组大小调整作业计数和步长可以提供更多改进(尽管在这个阶段是次要的)。

    最终,事实证明,并行化和可取消的实现可以为我的想法提供最佳的用户体验。

    【讨论】:

      【解决方案3】:

      首先,我认为处理您提到的问题的常用方法不是取消排序,而是在进行提取/排序操作之前添加延迟。

      用户通常会短时间输入。因此,在 fetch 和 sort 实际开始之前添加 x 秒的延迟(例如 0.5 秒)。

      例子:

      1. 用户类型“a”。
      2. 启动 x 秒计时器。
      3. 在计时器到期之前,用户类型“b”
      4. 使旧计时器失效,并在 x 秒后启动一个新计时器。
      5. 定时器到期,开始获取和排序操作。

      希望这会有所帮助。

      【讨论】:

      • 感谢您的想法,但如果排序本身花费的时间明显长于任意选择的延迟,则添加延迟并没有太大帮助。未完成操作的数量将减少,但未完成操作仍然存在。我还认为,低延迟数据源(本地计算机)上的预输入匹配应该尽可能快,并且添加任意延迟只会减慢用户体验。对于高延迟数据源,它可能更有意义。
      【解决方案4】:

      您可以实现自己的比较器,而不是实现自己的排序算法(检查取消),它可以检查取消条件,并抛出异常以中断 NSArray sortUsing... 对 NSArray sortUsing.. 的调用应包含在 try/catch 组中

      - (void) testInterruptSort
      {
          NSArray *a = @[@"beta", @"alpha", @"omega", @"foo", @"bar"];
           NSArray *sorted;
          BOOL interrupted = NO;
          NSString * const myException = @"User";
          @try {
               int __block n = 0;
              sorted = [a sortedArrayUsingComparator:^(NSString *s1, NSString *s2) {
                  if (/* cancel condition*/ (1) && (n>5)) {
                      n++;
                      NSException *e = [NSException exceptionWithName:myException reason:@"interrupted" userInfo:nil];
                      [e raise];
                  }
                   return [s1 localizedStandardCompare:s2];
              }];
          }
          @catch (NSException *exception) {
              // should check if this is the "User" exception
              // see       https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Exceptions/Tasks/HandlingExceptions.html
              interrupted = YES;
          }
          @finally {
          }
          NSLog(@"interrupted: %@, result = %@\n", interrupted ? @"YES":@"NO", sorted);
      }
      

      【讨论】:

      • 不幸的是,ARC 不能保证在异常情况下正确处理内存管理,并且您无法控制 sortedArrayUsingComparator: 在调用它和调用比较器之间可能会涉及动态内存。好主意,但是 Objective-C 会咬你 :-( (当然 ARC 理论上可以被编写来处理异常,这只是一个基于异常的设计决策,仅推荐用于 Objective-C 中的程序终止情况。)
      • 是的,很烦人... -fobjc-arc-exceptions 选项据说可以启用 ARC 异常支持,但会降低性能 - 但我从未尝试过。但是,它可能在这里不起作用(因为要展开的堆栈包含 NSArray 代码,我们不能假设使用 -fobjc-arc-exceptions 进行编译)..
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-20
      • 2015-07-15
      • 2011-06-01
      相关资源
      最近更新 更多