【问题标题】:Can NSBlockOperation cancel itself while executing, thus canceling dependent NSOperations?NSBlockOperation 是否可以在执行时自行取消,从而取消依赖的 NSOperation?
【发布时间】:2020-11-05 13:32:21
【问题描述】:

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

但是 - 如果只有我的操作的执行块“知道”它在执行时失败 - 它可以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 状态在块返回之前是YES。队列仍然像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

有什么想法吗?

【问题讨论】:

  • 听起来很熟悉,比如stackoverflow.com/questions/64671948/… 可能吗? PS在那里我给出了一个如何做到这一点的样本。在那个答案中,我们进行了很多讨论,最终我编辑了我的答案并对其进行了相当多的更改,但是我认为我的答案的第一部分中涵盖了您想要做什么以及示例实现。
  • PPS : 你有正确的想法,但需要同步你的条件并使用你自己的逻辑。
  • 我仔细阅读了参考资料,我的问题非常不同。首先,我的代码中没有任何异步,除了由并发 NSOperationQueue 运行的实际 NSOperations。另外 - 我不混合 GCD 和 NSOperation API。下一步 - 我不是在问如何实现实际取消 - 而是关于取消操作的影响 - (据我了解)它不像宣传的那样起作用(或者也许有人可以告诉我它是)

标签: objective-c macos objective-c-blocks nsoperationqueue nsoperation


【解决方案1】:

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

上面写着:

在 macOS 10.6 及更高版本中,如果操作在队列中但正在等待 未完成的依赖操作,这些操作随后 忽略。因为它已经被取消,所以这种行为允许 操作队列更快地调用操作的启动方法并清除 队列中的对象。如果您取消了一个不在 队列,此方法立即将对象标记为已完成。每个 情况下,将对象标记为就绪或完成会导致 生成适当的 KVO 通知。

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

那是一厢情愿……

文档说的是不同的。当您取消 B 时,尽管依赖于 A,但它不会等待 A 完成,然后再将其从队列中删除。如果 A 尚未完成 - 取消 B 将立即将其从队列中移除 - 因为它仍处于待处理状态(A 的完成)。

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

子类化 NSBlockOperation 可能是值得的,如果任何“依赖项”已被取消,则覆盖调用“开始”以跳到完成的逻辑......但这是一个长期的目标,可能难以实现。

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

@interface MYBlockOperation : NSBlockOperation {
}
@end

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

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

【讨论】:

  • 是的,这就是我一直在尝试交流的内容。但我不认为像你提到的那样子类化是一个好主意 - 也可以看看我的其他 cmets 并真正回答,看看我建议你如何解决这个问题。但这是我的意见,所以让我知道你是怎么做的。
  • 我认为这可能是在代码中传达我的依赖概念的最简单和最直接的方式......如果我依赖的任何操作被取消 - 在开始之前取消自己(然后以优雅的方式开始,立即跳到“完成”,因为我被取消了。所以 - 长链也会保留这种行为。
  • 如果您必须从一开始就安排好所有块,那么执行聚合键值表达式的方式非常简洁。
  • 但请注意,您的实施行中有太多 T ...
  • 谢谢,我把多余的T去掉了。我需要堆积上万次的操作才能批量处理上千张照片。每张照片经过 5-15 个处理步骤,每张照片略有不同(取决于初始步骤的结果)。您可以看到广泛使用 NSOperations 的 NSOperationQueue “图”。我有 5 个不同的队列,具有不同的优先级和 QOS,必须独立地暂停/恢复每个队列,所以......对我来说,“依赖”不仅仅是设置执行顺序,而是更多“依赖”之前 NSOperations 的结果.
【解决方案2】:

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

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

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

这是解释,我可以给出它的示例实现,但我认为您可以通过查看代码自己做到这一点?

编辑

如果您有很多依赖并按顺序执行的块,您甚至不需要操作队列,或者您只需要一个串行(一次 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代表了一个更大的整体,例如清理数据或拍照或任何您试图解决的大问题。

此外,如果您的工作速度非常慢并且需要挤出性能,您可以通过找出(子和子子)任务之间的依赖关系并尽快启动它们来做到这一点。这种类型的校准是(现实生活中)问题在 UI 上花费了太长时间的地方变得可行,因为您可以将它们分解并(非线性)将它们拼凑在一起,以便您可以以最有效的方式遍历它们.

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

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

此外,在这里呈现的方式,当您在解决问题方面取得进展时,在任务 1 和 2 或 2 和 3 之间的那些接合点,您可以在这些地方更新您的 UI 进度和部分完整的解决方案,因为它从所有各种(子和子)任务中渗透出来。

(末日来临)EDIT 6

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

【讨论】:

  • 但这击败了整个操作依赖点?我编写了多线程代码很多年,并且非常了解如何使用 dispatch_groups 来创建依赖项 --- 然而,NSOperationQueue 被设计为天生支持这一点 - 文档说得很清楚 - 如果取消操作,依赖操作将不会开始。
  • 多年来一直在做并发的事情,你总是不得不取消自己。如果您将取消的操作添加到队列中,则它只是被认为已完成并且依赖关系立即开始。
  • 就依赖关系而言,取消与完成相同。
  • 我的许多操作都是独立的,我分布在几个不同的队列中(根据优先级、服务质量和同时运行的能力)。但是,当 4 个这样的操作完成(成功)时,下一个阶段(另一个操作)可以开始。因此,我将下一阶段的操作设置为依赖于这 4 个操作 - 并调度它。依赖项确实有效 - 在所有 4 个完成之前它永远不会运行。但是 - 文档说如果 4 个中的一个被取消 - 下一个阶段不应该运行,它会运行。
  • 所以......你从一开始就是对的,你最短的评论(我点赞的那个)应该作为答案发布:)
猜你喜欢
  • 2015-03-20
  • 1970-01-01
  • 1970-01-01
  • 2011-12-28
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多