NSBlockOperation 能否在执行时取消自身,从而取消依赖的 NSOperations?

Can NSBlockOperation cancel itself while executing, thus canceling dependent NSOperations?

我有许多 NSBlockOperation 具有依赖关系的链。如果链中早期的一个操作失败——我希望其他操作不会 运行。根据文档,这应该很容易从外部完成 - 如果我取消操作,所有相关操作都应该自动取消。

但是 - 如果只有我的操作的执行块在执行时“知道”它失败了 - 它可以 cancel 它自己的工作吗?

我尝试了以下方法:

    NSBlockOperation *op = [[NSBlockOperation alloc] init];
    __weak NSBlockOperation *weakOpRef = op;
    [takeScreenShot addExecutionBlock:^{
        LOGInfo(@"Say Cheese...");
        if (some_condition == NO) { // for some reason we can't take a photo
            [weakOpRef cancel];
            LOGError(@"Photo failed");
        }
        else {
            // take photo, process it, etc.
            LOGInfo(@"Photo taken");
        }
    }];

但是,当我 运行 执行此操作时,即使取消了 op,也会执行依赖于 op 的其他操作。由于它们是相关的 - 当然它们不会在 op 完成之前开始,并且我验证了(在调试器中并使用日志) opisCancelled 状态在 op 之前是 YES块 returns。队列仍然执行它们,就好像 op 成功完成一样。

然后我进一步挑战了文档,如下所示:

    NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *op = [[NSBlockOperation alloc] init];
    __weak NSBlockOperation *weakOpRef = takeScreenShot;
    [takeScreenShot addExecutionBlock:^{
        NSLog(@"Say Cheese...");
        if (weakOpRef.isCancelled) { // Fail every once in a while...
            NSLog(@"Photo failed");
        }
        else {
            [NSThread sleepForTimeInterval:0.3f];
            NSLog(@"Photo taken");
        }
    }];
    
    NSOperation *processPhoto = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"Processing Photo...");
        [NSThread sleepForTimeInterval:0.1f]; // Process  
        NSLog(@"Processing Finished.");
    }];
    
    // setup dependencies for the operations.
    [processPhoto addDependency: op];
    [op cancel];    // cancelled even before dispatching!!!
    [myQueue addOperation: op];
    [myQueue addOperation: processPhoto];
    
    NSLog(@">>> Operations Dispatched, Wait for processing");
    [eventQueue waitUntilAllOperationsAreFinished];
    NSLog(@">>> Work Finished");

但是在日志中看到如下输出吓坏了:

2020-11-05 16:18:03.803341 >>> Operations Dispatched, Wait for processing
2020-11-05 16:18:03.803427 Processing Photo...
2020-11-05 16:18:03.813557 Processing Finished.
2020-11-05 16:18:03.813638+0200 TesterApp[6887:111445] >>> Work Finished

注意:被取消的操作从来都不是 运行 - 但依赖的 processPhoto 被执行了,尽管它依赖于 op.

有人有想法吗?

如果您取消一个操作,您只是提示它已经完成,特别是在长时间的运行ning 任务中,您必须自己实现逻辑。 如果您取消某些内容,依赖项将认为任务已完成并且 运行 没问题。

因此,您需要做的是拥有某种全局同步变量,您可以以同步方式设置和获取该变量,并且应该捕获您的逻辑。您的 运行ning 操作应定期并在关键点检查该变量并自行退出。请不要使用实际的 global,而是使用一些所有进程都可以访问的公共变量 - 我想你会很乐意实施这个?

Cancel 并不是从 运行ning 开始停止操作的灵丹妙药,它只是对调度程序的提示,允许它优化内容。取消你必须自己做。

这是解释,我可以给出它的示例实现,但我认为您可以通过查看代码自行完成?

编辑

如果你有很多依赖并按顺序执行的块,你甚至不需要操作队列,或者你只需​​要一个串行(一次 1 个操作)队列。如果块按顺序执行但非常不同,那么您需要在条件失败时处理不添加新块的逻辑。

编辑 2

关于我建议您如何解决这个问题的一些想法。当然细节很重要,但这也是一种很好的直接方式。这是一种伪代码,所以不要迷失在语法中。

// Do it all in a class if possible, not subclass of NSOpQueue
class A

  // Members
  queue

  // job1
  synced state cancel1    // eg triggered by UI
  synced state counter1
  state calc1 that job 1 calculates (and job 2 needs)

  synced state cancel2
  synced state counter2
  state calc2 that job 2 calculated (and job 3 needs)
  ...

start
  start on queue

    schedule job1.1 on (any) queue
       periodically check cancel1 and exit
       update calc1
       when done or exit increase counter1

    schedule job1.2 on (any) queue
       same
    schedule job1.3
       same

  wait on counter1 to reach 0
  check cancel1 and exit early

  // When you get here nothing has been cancelled and
  // all you need for job2 is calculated and ready as
  // state1 in the class.
  // This is why state1 need not be synced as it is
  // (potentially) written by job1 and read by job2
  // so no concurrent access.

    schedule job2.1 on (any) queue

   and so on

这是对我来说最直接也最适合未来发展的做法。易于维护和理解等。

编辑 3

我喜欢和喜欢它的原因是因为它将所有相互依赖的逻辑放在一个地方,如果您需要更好的控制,以后很容易添加或校准它。

我喜欢这个的原因子类化 NSOp 就是将这个逻辑分散到许多已经很复杂的子类中,同时你也失去了一些控制。在这里,您仅在测试了一些条件并知道下一批需要 运行 之后才安排内容。在替代方案中,您一次安排所有内容,并且需要在所有子类中添加额外的逻辑来监视任务的进度或取消的状态,以便它迅速增加。

如果子类中 运行 的特定操作需要校准,我将对 NSOp 进行子类化,但将其子类化以管理相互依赖性会增加我侦察的复杂性。

(可能是最终版本)编辑 4

如果你能做到这一点,我印象深刻。现在,看看我提议的一段(伪)代码,您可能会发现它有点矫枉过正,您可以大大简化它。这是因为它的呈现方式,整个任务的不同组成部分,即任务 1、任务 2 等等,似乎是断开连接的。如果是这种情况,确实有许多不同且更简单的方法可以做到这一点。如果所有任务都相同或非常相似,或者如果每个子任务(例如 1)只有一个子子任务(例如 1.1)或只有一个(子或子子)任务,我在参考资料中给出了一个很好的方法运行宁在任何时间点。

但是,对于实际问题,您最终可能会在这些问题之间获得更少的干净和线性流程。换句话说,在任务 2 说你可以启动任务 3.1 之后,任务 4 或 5 不需要,但任务 6 只需要。然后取消和退出早期逻辑已经变得棘手,我不打破这个的原因分成更小和更简单的位实际上是因为像这里一样,逻辑也可以(轻松地)跨越这些子任务,因为这个 class A 代表一个更大的整体,例如清理数据或拍照或任何你试图解决的大问题。

另外,如果你做的事情真的很慢并且你需要挤出性能,你可以通过找出(sub 和 subsub)任务之间的依赖关系并尽快启动它们来做到这一点。这种类型的校准是在 UI 花费太长时间的(现实生活)问题变得可行的地方,因为您可以将它们分解并(non-linearly)以您可以遍历的方式将它们拼凑在一起他们以最有效的方式。

我遇到过一些这样的问题,特别是我认为知道的问题变得非常脆弱并且逻辑难以遵循,但是通过这种方式我能够将解决方案时间从不可接受的时间缩短到超过一分钟到几秒钟,让用户满意。

(这次真的快到决赛了)EDIT 5

此外,这里呈现的方式是,随着您在解决问题方面取得进展,在任务 1 和任务 2 之间或任务 2 和任务 3 之间的那些时刻,您可以在这些地方更新您的 UI 随着它从各种(sub 和 subsb) 任务。

(末日来临)编辑 6

如果您在单个核心上工作,那么除了任务之间的相互依赖性之外,您安排所有这些子任务和子子任务的顺序并不重要,因为执行是线性的。当您拥有多个内核时,您需要将解决方案分解为尽可能小的子任务,并尽快安排较长的 运行ning 子任务以提高性能。您获得的性能压缩可能很重要,但代价是所有小的小子任务之间的流程越来越复杂,并且您处理取消逻辑的方式也越来越复杂。

好的。我想我解开了这个谜。我只是误解了 [NSOperation cancel] documentation.

它说:

In macOS 10.6 and later, if an operation is in a queue but waiting on unfinished dependent operations, those operations are subsequently ignored. Because it is already cancelled, this behavior allows the operation queue to call the operation’s start method sooner and clear the object out of the queue. If you cancel an operation that is not in a queue, this method immediately marks the object as finished. In each case, marking the object as ready or finished results in the generation of the appropriate KVO notifications.

我认为如果操作 B 依赖于操作 A - 这意味着当 A 被取消时(因此 - 没有完成它的工作)那么 B 也应该被取消 - 因为在语义上它不能开始直到 A完成了它的工作。

那是一厢情愿的想法...

文档所说的不同。当您取消 B 时,尽管它依赖于 A - 它不会等待 A 在从队列中删除之前完成。如果 A 尚未完成 - 取消 B 将立即从队列中删除它 - 因为它仍在等待(A 的完成)。

Soooo.....为了完成我的方案,我需要引入我自己的“依赖”机制,可能以布尔属性集的形式,如 isPhotoTakenisPhotoProcessed 等. 然后是依赖于这些的操作,将需要检查其前导码(执行块)是否所有必需的先前操作实际上已成功完成。

可能值得将 NSBlockOperation 子类化,覆盖调用 'start' 的逻辑以在 'dependencies' 中的任何一个已被取消时跳至完成...但这是一个远景,可能很难实施。

最后,我写了这个快速子类,它似乎工作了——当然需要进行更深入的检查:

@interface MYBlockOperation : NSBlockOperation {
}
@end

@implementation MYBlockOperation
- (void)start {
    if ([[self valueForKeyPath:@"dependencies.@sum.cancelled"] intValue] > 0)
        [self cancel];
    [super start];
}
@end

当我在原始问题(以及我的其他测试)中将 NSBlockOperation 替换为 MYBlockOperation 时,行为与我预期的一样。