Search code examples
iosswiftgrand-central-dispatchnsoperationqueue

How to suspend dispatch queue inside for loop?


I have play and pause button. When I pressed play button, I want to play async talking inside for loop. I used dispatch group for async method's waiting inside for loop. But I cannot achieve pause.

startStopButton.rx.tap.bind {
    if self.isPaused {
        self.isPaused = false
        dispatchGroup.suspend()
        dispatchQueue.suspend()
    } else {
        self.isPaused = true
        self.dispatchQueue.async {
            for i in 0..<self.textBlocks.count {
                self.dispatchGroup.enter()
                self.startTalking(string: self.textBlocks[i]) { isFinished in
                    self.dispatchGroup.leave()
                }
                self.dispatchGroup.wait()
            }
        }
    }
}.disposed(by: disposeBag)

And i tried to do with operationqueue but still not working. It is still continue talking.

startStopButton.rx.tap.bind {
    if self.isPaused {
        self.isPaused = false
        self.talkingQueue.isSuspended = true
        self.talkingQueue.cancelAllOperations()
    } else {
        self.isPaused = true
        self.talkingQueue.addOperation {
            for i in 0..<self.textBlocks.count {
                self.dispatchGroup.enter()
                self.startTalking(string: self.textBlocks[i]) { isFinished in
                    self.dispatchGroup.leave()
                }
                self.dispatchGroup.wait()
            }
        }
    }
}.disposed(by: disposeBag)

Is there any advice?


Solution

  • A few observations:

    1. Pausing a group doesn’t do anything. You suspend queues, not groups.

    2. Suspending a queue stops new items from starting on that queue, but it does not suspend anything already running on that queue. So, if you’ve added all the textBlock calls in a single dispatched item of work, then once it’s started, it won’t suspend.

      So, rather than dispatching all of these text blocks to the queue as a single task, instead, submit them individually (presuming, of course, that your queue is serial). So, for example, let’s say you had a DispatchQueue:

      let queue = DispatchQueue(label: "...")
      

      And then, to queue the tasks, put the async call inside the for loop, so each text block is a separate item in your queue:

      for textBlock in textBlocks {
          queue.async { [weak self] in
              guard let self = self else { return }
      
              let semaphore = DispatchSemaphore(value: 0)
      
              self.startTalking(string: textBlock) {
                  semaphore.signal()
              }
              semaphore.wait()
          }
      }
      

      FYI, while dispatch groups work, a semaphore (great for coordinating a single signal with a wait) might be a more logical choice here, rather than a group (which is intended for coordinating groups of dispatched tasks).

      Anyway, when you suspend that queue, the queue will be preventing from starting anything queued (but will finish the current textBlock).

    3. Or you can use an asynchronous Operation, e.g., create your queue:

      let queue: OperationQueue = {
          let queue = OperationQueue()
          queue.name = "..."
          queue.maxConcurrentOperationCount = 1
          return queue
      }()
      

      Then, again, you queue up each spoken word, each respectively a separate operation on that queue:

      for textBlock in textBlocks {
          queue.addOperation(TalkingOperation(string: textBlock))
      }
      

      That of course assumes you encapsulated your talking routine in an operation, e.g.:

      class TalkingOperation: AsynchronousOperation {
          let string: String
      
          init(string: String) {
              self.string = string
          }
      
          override func main() {
              startTalking(string: string) {
                  self.finish()
              }
          }
      
          func startTalking(string: String, completion: @escaping () -> Void) { ... }
      }
      

      I prefer this approach because

      • we’re not blocking any threads;
      • the logic for talking is nicely encapsulated in that TalkingOperation, in the spirit of the single responsibility principle; and
      • you can easily suspend the queue or cancel all the operations.
         

      By the way, this is a subclass of an AsynchronousOperation, which abstracts the complexity of asynchronous operation out of the TalkingOperation class. There are many ways to do this, but here’s one random implementation. FWIW, the idea is that you define an AsynchronousOperation subclass that does all the KVO necessary for asynchronous operations outlined in the documentation, and then you can enjoy the benefits of operation queues without making each of your asynchronous operation subclasses too complicated.

    4. For what it’s worth, if you don’t need suspend, but would be happy just canceling, the other approach is to dispatching the whole for loop as a single work item or operation, but check to see if the operation has been canceled inside the for loop:

      So, define a few properties:

      let queue = DispatchQueue(label: "...")
      var item: DispatchWorkItem?
      

      Then you can start the task:

      item = DispatchWorkItem { [weak self] in
          guard let textBlocks = self?.textBlocks else { return }
      
          for textBlock in textBlocks where self?.item?.isCancelled == false {
              let semaphore = DispatchSemaphore(value: 0)
              self?.startTalking(string: textBlock) {
                  semaphore.signal()
              }
              semaphore.wait()
          }
          self?.item = nil
      }
      
      queue.async(execute: item!)
      

      And then, when you want to stop it, just call item?.cancel(). You can do this same pattern with a non-asynchronous Operation, too.