简体   繁体   English

NSBlockOperation 可以在执行时取消自身,从而取消依赖的 NSOperations 吗?

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

I have a chain of many NSBlockOperation s with dependencies.我有许多具有依赖关系的NSBlockOperation链。 If one operation early in the chain fails - I want the other operations to not run.如果链中早期的一个操作失败 - 我希望其他操作不运行。 According to docs, this should be easy to do from the outside - if I cancel an operation, all dependent operations should automatically be cancelled.根据文档,这应该很容易从外部完成 - 如果我取消一个操作,所有相关操作都应该自动取消。

However - if only the execution-block of my operation "knows" that it failed, while executing - can it cancel its own work?但是 - 如果只有我的操作的执行块“知道”它在执行时失败了 - 它可以cancel自己的工作吗?

I tried the following:我尝试了以下方法:

    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");
        }
    }];

However, when I run this, other operations dependent on op are executed even though op was cancelled.但是,当我运行它时,即使op被取消,依赖于op其他操作也会被执行。 Since they are dependent - surely they're not starting before op finished, and I verified (in debugger and using logs) that isCancelled state of op is YES before the block returns.由于它们是相关的 - 当然它们不会在op完成之前启动,并且我验证(在调试器和使用日志中)在块返回之前op isCancelled状态为YES Still the queue executes them as if op finished successfully.队列仍然执行它们,就好像op成功完成一样。

I then further challenged the docs, like thus:然后我进一步挑战了文档,如下所示:

    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");

But was horrified to see the following output in the log:但是在日志中看到以下输出时感到震惊:

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

Pay attention: the cancelled op was never run - but the dependent processPhoto was executed, despite its dependency on op .注意:取消的 op 从未运行 - 但依赖processPhoto被执行,尽管它依赖于op

Ideas anyone?任何人的想法?

If you cancel an operation you just hint that it is done, especially in long running tasks you have to implement the logic yourself.如果你取消一个操作,你只是暗示它已经完成,尤其是在长时间运行的任务中,你必须自己实现逻辑。 If you cancel something the dependencies will consider the task finished and run no problem.如果您取消某些东西,依赖项将认为任务已完成并且运行没有问题。

So what you need to do is have some kind of a global synced variable that you set and get in a synced fashion and that should capture your logic.因此,您需要做的是拥有某种全局同步变量,您可以设置并以同步方式获取,并且应该捕获您的逻辑。 Your running operations should check that variable periodically and at critical points and exit themselves.您正在运行的操作应该定期和在关键点检查该变量并自行退出。 Please don't use actual global but use some common variable that all processes can access - I presume you will be comfortable in implementing this?请不要使用实际的全局变量,而是使用一些所有进程都可以访问的公共变量 - 我想你会很乐意实现这个吗?

Cancel is not a magic bullet that stop the operation from running, it is merely a hint to the scheduler that allows it to optimise stuff. Cancel 不是阻止操作运行的灵丹妙药,它只是对调度程序的提示,允许它优化内容。 Cancel you must do yourself.取消你必须自己做。

This is explanation, I can give sample implementation of it but I think you are able to do that on your own looking at the code?这是解释,我可以提供它的示例实现,但我认为您可以自己查看代码来做到这一点?

EDIT编辑

If you have a lot of blocks that are dependent and execute sequentially you do not even need an operation queue or you only need a serial (1 operation at a time) queue.如果您有许多相互依赖并按顺序执行的块,您甚至不需要操作队列,或者您只需要一个串行(一次 1 个操作)队列。 If the blocks execute sequentially but are very different then you need to rather work on the logic of NOT adding new blocks once the condition fails.如果块按顺序执行但非常不同,那么您需要在条件失败后不添加新块的逻辑上工作。

EDIT 2编辑 2

Just some idea on how I suggest you tackle this.只是关于我建议你如何解决这个问题的一些想法。 Of course detail matters but this is also a nice and direct way of doing it.当然细节很重要,但这也是一种很好且直接的方式。 This is sort of pseudo code so don't get lost in the syntax.这是一种伪代码,所以不要迷失在语法中。

// 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

This is to me most direct and ready for future development way of doing it.这对我来说是最直接和为未来发展做好准备的方式。 Easy to maintain and understand and so on.易于维护和理解等。

EDIT 3编辑 3

Reason I like and prefer this is because it keeps all your interdependent logic in one place and it is easy to later add to it or calibrate it if you need finer control.我喜欢并喜欢它的原因是因为它将所有相互依赖的逻辑保存在一个地方,如果您需要更好的控制,很容易在以后添加或校准它。

Reason I prefer this to eg subclassing NSOp is that then you spread out this logic into a number of already complex subclasses and also you loose some control.我更喜欢这个,例如子类化 NSOp 的原因是,你把这个逻辑扩展到许多已经很复杂的子类中,并且你也失去了一些控制。 Here you only schedule stuff after you've tested some condition and know that the next batch needs to run.在这里,您仅在测试了某些条件并知道下一批需要运行后才安排工作。 In the alternative you schedule all at once and need additional logic in all subclasses to monitor progress of the task or state of the cancel so it mushrooms quickly.在替代方案中,您一次安排所有时间并需要所有子类中的附加逻辑来监视任务的进度或取消的状态,以便它快速增长。

Subclassing NSOp I'd do if the specific op that run in that subclass needs calibration, but to subclass it to manage the interdependencies adds complexity I recon.子类化 NSOp 如果在该子类中运行的特定 op 需要校准,我会这样做,但是将其子类化以管理相互依赖性会增加我认为的复杂性。

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

If you made it this far I am impressed.如果你做到了这一点,我印象深刻。 Now, looking at my proposed piece of (pseudo) code you might see that it is overkill and that you can simplify it considerably.现在,看看我提议的一段(伪)代码,你可能会发现它是矫枉过正的,而且你可以大大简化它。 This is because the way it is presented, the different components of the whole task, being task 1, task 2 and so on, appear to be disconnected.这是因为它的呈现方式,整个任务的不同组成部分,即任务 1、任务 2 等,似乎是断开的。 If that is the case there are indeed a number of different and simpler ways in which you can do this.如果是这种情况,确实有许多不同且更简单的方法可以做到这一点。 In the reference I give a nice way of doing this if all the tasks are the same or very similar or if you have only a single subsubtask (eg 1.1) per subtask (eg 1) or only a single (sub or subsub) task running at any point in time.如果所有任务都相同或非常相似,或者如果每个子任务(例如 1)只有一个子子任务(例如 1.1)或只有一个(子或子子)任务正在运行,我在参考文献中给出了一个很好的方法在任何时间点。

However, for real problems, you will probably end up with much less of a clean and linear flow between these.但是,对于实际问题,您最终可能会发现这些问题之间的干净和线性流程要少得多。 In other words, after task 2 say you may kick of task 3.1 which is not required by task 4 or 5 but only needed by task 6. Then the cancel and exit early logic already becomes tricky and the reason I do not break this one up into smaller and simpler bits is really because like here the logic can (easily) also span those subtasks and because this class A represents a bigger whole eg clean data or take pictures or whatever your big problem is that you try to solve.换句话说,在任务 2 之后说你可以踢任务 4 或任务 5 不需要的任务 3.1,但任务 6 只需要。然后取消和退出早期逻辑已经变得棘手,这也是我不打破这个的原因变成更小更简单的位真的是因为像这里一样,逻辑也可以(很容易)跨越这些子任务,因为这个class A代表一个更大的整体,例如清理数据或拍照或任何你试图解决的大问题。

Also, if you work on something that is really slow and you need to squeeze out performance, you can do that by figuring out the dependencies between the (sub and subsub) tasks and kick them off asap.此外,如果您处理的工作非常缓慢并且需要挤出性能,您可以通过确定(子和子子)任务之间的依赖关系并尽快将它们启动来做到这一点。 This type of calibration is where (real life) problems that took way too long for the UI becomes doable as you can break them up and (non-linearly) piece them together in such a way that you can traverse them in a most efficient way.这种类型的校准是在 UI 上花费太长时间的(现实生活中的)问题变得可行的地方,因为您可以将它们分解并(非线性地)以一种可以最有效的方式遍历它们的方式将它们拼凑在一起.

I've had a few such a problems and, one in particular I am thinking know became extremely fragile and the logic difficult to follow, but this way I was able to bring the solution time down from an unacceptable more than a minute to just a few seconds and agreeable to the users.我遇到过一些这样的问题,特别是我认为有一个问题变得非常脆弱,逻辑难以理解,但通过这种方式,我能够将解决时间从不可接受的超过一分钟缩短到仅几秒钟,让用户满意。

(This time really almost the final) EDIT 5 (这次真的快到决赛了)EDIT 5

Also, the way it is presented here, as you make progress in solving the problem, at those junctures between say task 1 and 2 or between 2 and 3, those are the places where you can update your UI with progress and parts of the full solution as it trickles in from all the various (sub and subsub) tasks.此外,这里呈现的方式,随着您在解决问题方面取得进展,在任务 1 和 2 之间或任务 2 和 3 之间的那些接合点,您可以在这些地方更新您的 UI 进度和部分完整内容解决方案,因为它从所有各种(子和子子)任务中涓涓细流。

(The end is coming) EDIT 6 (末日来了)编辑 6

If you work on a single core then, except for the interdependencies between tasks, the order in which you schedule all those sub and subsub tasks do not matter since execution is linear.如果您在单个内核上工作,那么除了任务之间的相互依赖性外,您安排所有这些子任务和子子任务的顺序无关紧要,因为执行是线性的。 The moment you have multiple cores you need to break the solution up into as small as possible subtasks and schedule the longer running ones asap for performance.当您拥有多个内核时,您需要将解决方案分解成尽可能小的子任务,并尽快安排运行时间较长的任务以提高性能。 The performance squeeze you get can be significant but comes at the cost of increasingly complex flow between all the small little subtasks and in the way in which you handle the cancel logic.您获得的性能压力可能很大,但代价是所有小的子任务之间的流程越来越复杂,以及您处理取消逻辑的方式。

OK.好的。 I think I solved the mystery.我想我解开了这个谜。 I just misunderstood the [NSOperation cancel] documentation .我只是误解了[NSOperation cancel] 文档

it says:它说:

In macOS 10.6 and later, if an operation is in a queue but waiting on unfinished dependent operations, those operations are subsequently ignored.在 macOS 10.6 及更高版本中,如果某个操作在队列中但正在等待未完成的相关操作,则这些操作随后将被忽略。 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.因为它已经被取消,这个行为允许操作队列更快地调用操作的 start 方法并将对象从队列中清除。 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.在每种情况下,将对象标记为就绪或完成都会导致生成适当的 KVO 通知。

I thought that if operation B depends on operation A - it means that when A is canceled (hence - didn't finish its work) then B should be cancelled as well - because semantically it can't start until A completed its work.我认为如果操作 B 依赖于操作 A - 这意味着当 A 被取消时(因此 - 没有完成它的工作),那么 B 也应该被取消 - 因为从语义上讲,它在 A 完成其工作之前无法开始。

That's wishful thinking...真是一厢情愿……

What the docs say is different.文档说的是不同的。 When you cancel B, then despite being dependent on A - it won't wait for A to finish before it's removed from the queue.当您取消 B 时,尽管依赖于 A - 它不会等待 A 完成才将其从队列中删除。 If A hasn't finished yet - canceling B will remove it quite immediately from the queue - because it's still pending (the completion of A).如果 A 还没有完成 - 取消 B 会立即将它从队列中移除 - 因为它仍然处于挂起状态(A 的完成)。

Soooo..... to accomplish my scheme, I will need to introduce my own "dependencies" mechanism, probably in the form of set of boolean properties like isPhotoTaken , isPhotoProcessed etc. and then an operation dependent on these, will need to check in its preamble (of execution block) whether all the required previous operations actually finished successfully. Soooo ..... 为了完成我的计划,我需要引入我自己的“依赖”机制,可能以一组布尔属性的形式,如isPhotoTakenisPhotoProcessed等,然后依赖于这些的操作,需要检查在其序言(执行块)中是否所有所需的先前操作实际上都成功完成。

It may be worth subclassing NSBlockOperation, overriding the logic that calls 'start' to skip to finished if any of the 'dependencies' has been cancelled... but that's a long shot and may be hard to implement.可能值得继承 NSBlockOperation,覆盖调用“开始”的逻辑,如果任何“依赖项”被取消,则跳到完成……但这是一个很长的镜头,可能很难实现。

Finally, I wrote this quick subclass, and it seems to work - of course deeper inspection is due:最后,我写了这个快速子类,它似乎有效——当然更深入的检查是应该的:

@interface MYBlockOperation : NSBlockOperation {
}
@end

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

When I substitute NSBlockOperation with MYBlockOperation in the original question (and my other tests, the behaviour is as the one I expected.当我在原始问题(以及我的其他测试)中用 MYBlockOperation 替换 NSBlockOperation 时,行为与我预期的一样。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM