0

I have some operations that need to run synchronously. I tried to follow this link but it's not clear enough for my situation.

op2 doesn't start until op1 is finished and op3 doesn't start until op2 is finished but during that time I need to be able to stop any of the operations and restart all over again. For example if op2 is running, I know that it cannot be stopped, but for whatever reason I need to be able to prevent op3 from executing because op1 has restarted. How can I do this?

This is a very simple example, the actual code is more intricate

var queue1 = OperationQueue()
var queue2 = OperationQueue()
var queue3 = OperationQueue()
     
var operation1: BlockOperation?
var operation2: BlockOperation?
var operation3: BlockOperation?

// a DispatchGroup has finished running now it's time to start the operations ...
dispatchGroup.notify(queue: .global(qos: .background)) { [weak self] in
    DispatchQueue.main.async { [weak self] in

        self?.runFirstFunc()
    }
}

func runFirstFunc() {

   var count = 0

   let num in arr  {
       count += num
   }

   // now that the loop is finished start the second func but there is a possibility something may happen in the first that should prevent the second func from running
   runSecondFunc(count: count)
}

func runSecondFunc(count: Int) {

    do {

        try ...

        // if the do-try is successful do something with count then start thirdFunc but there is a possibility something may happen in the second func that should prevent the third func from running
        runThirdFunc()

    } catch {
        return
    }
}

func runThirdFunc() {

    // this is the final operation, once it hits here I know it can't be stopped even if I have to restart op1 again but that is fine
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • Have a look at `AsynchronousOperation` suggested in [this question](https://stackoverflow.com/questions/43561169/trying-to-understand-asynchronous-operation-subclass/48104095#48104095) – vadian Jun 15 '21 at 19:51
  • cool, thanks! I'll take a look – Lance Samaria Jun 15 '21 at 19:52
  • @vadian I just looked it over. The way i understood that thread is that it's about subclassing Operations. I don't see how that applies to what I'm asking. Maybe it's going over my head? – Lance Samaria Jun 15 '21 at 19:56
  • Is there a pressing reason to use OperationQueue? There are easy and comprehensible solutions with plain GCD and Swift Combine. Note that implementing Operations is error prone and they will become unduly complex (if done correctly) if you want to chain output from A, to input of B. – CouchDeveloper Jun 16 '21 at 11:09
  • @CouchDeveloper no, there isn’t any specific reason to use OperationQueues. I would normally use DispatchWorkItems but I don’t see how those will work in this situation. I haven’t had a chance to try taruntyagi answer yet. Why are OperationQueus error prone? I honestly only used them once for something and that something didn’t work so I abandoned them. I have zero experience with OQs so I assumed OQs were a good fit. – Lance Samaria Jun 16 '21 at 11:42
  • It's not the operation _queue_, it's the operations which become more elaborate to implement, if you want to make them error free. Many implementations of Operation and the surrounding code implicitly assume _thread confinement_, means, it is assumed, you call it on one certain thread only, mostly the main thread, and any value that is passed to them is accessed only on that thread, too. – CouchDeveloper Jun 16 '21 at 11:49
  • @CouchDeveloper all the functions and everything contained inside them (values) all happen on the main thread. – Lance Samaria Jun 16 '21 at 11:53

3 Answers3

1

You said:

op2 doesn't start until op1 is finished and op3 doesn't start until op2 is finished ...

If using OperationQueue you can accomplish that by creating the three operations, and defining op1 to be a dependency of op2 and defining op2 as a dependency of op3.

... but during that time I need to be able to stop any of the operations and restart all over again.

If using OperationQueue, if you want to stop all operations that have been added to the queue, you call cancelAllOperations.

For example if op2 is running, I know that it cannot be stopped, ...

Well, it depends upon what op2 is doing. If it's spinning in a loop doing calculations, then, yes, it can be canceled, mid-operation. You just check isCancelled, and if it is, stop the operation in question. Or if it is a network request (or something else that is cancelable), you can override cancel method and cancel the task, too. It depends upon what the operation is doing.

... but for whatever reason I need to be able to prevent op3 from executing because op1 has restarted.

Sure, having canceled all the operations with cancelAllOperations, you can then re-add three new operations (with their associated dependencies) to the queue.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
0

Here's a not-tested implementation that allows cancellation while any task is doing it's subtasks (repeatedly).

In case second task fails/throws, it automatically restarts from the first task.

In case user manually stops / starts, the last in-flight task quits it's execution (as soon as it can).

Note : You must take care of [weak self] part according to your own implementation.

import Foundation

class TestWorker {
    let workerQueue = DispatchQueue.global(qos: .utility)
    var currentWorkItem: DispatchWorkItem?
    
    func start() {
        self.performTask { self.performTask1() }
    }
    
    func stop() {
        currentWorkItem?.cancel()
    }
    
    func performTask(block: @escaping (() -> Void)) {
        let workItem = DispatchWorkItem(block: block)
        self.currentWorkItem = workItem
        workerQueue.async(execute: workItem)
    }
    
    func performTask1() {
        guard let workItem = self.currentWorkItem else { return }
        func subtask(index: Int) {}
        for i in 0..<100 {
            if workItem.isCancelled { return }
            subtask(index: i)
        }
        self.performTask { self.performTask2() }
    }
    
    func performTask2() {
        guard let workItem = self.currentWorkItem else { return }
        func subtask(index: Int) throws {}
        for i in 0..<100 {
            if workItem.isCancelled { return }
            do { try subtask(index: i) }
            catch { 
                self.start()
                return
            }
        }
        self.performTask { self.performTask3() }
    }
    
    func performTask3() {
        guard let workItem = self.currentWorkItem else { return }
        func subtask(index: Int) {}
        for i in 0..<100 {
            if workItem.isCancelled { return }
            subtask(index: i)
        }
        /// Done
    }
}
Tarun Tyagi
  • 9,364
  • 2
  • 17
  • 30
  • It seems, you didn't avoid data races when accessing `currentWorkItem`. So, `stop()` may "sometimes" not work. It can be done, utilising a synchronisation queue and a task checking for the cancellation state of the previous state before running, or creating the worker items a priory and holding them in an array, where `stop()` cancels them all. – CouchDeveloper Jun 16 '21 at 11:31
0

Maybe, this is a good reason to look into Swift Combine:

  • Define your tasks as Publishers.
  • Use flatMap to chain them, optionally pass output from previous to the next.
  • Use switchToLatest to restart the whole thing and cancel the previous one when it is still running - if any.
  • Use cancel on the subscriber to cancel the whole thing.
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67