Skip to content →

Tag: GCD

Delayed Preemptive Dispatch

DelayedPreemptiveDispatchQueueDemoIf you’re looking to run a task closure concurrently using Grand Central Dispatch after some delay … with the option to delay it again, replace it with another task, or cancel it altogether, the concept of delayed preemptive dispatch presented here may be of interest to you. For a quick peek, skip to Implementation; for use cases, read on.

Problem Scenarios

Consider these problem scenarios, all taken from some of my recent work. How would you implement the described functionality?

  1. You want to allow a user to make three incorrect passcode entries within a minute of each other before locking them out. If more than a minute elapses between entries, the try count resets to zero.
  2. You are using a third party framework that presents a panning view. Its API provides the means for notification as each pan motion stops…both when the pan comes to a rest and when the user interrupts one pan motion to start another. However, you need to know when all panning has stopped.
  3. You are interfacing with a third party library that concurrently retrieves an unknown quantity of online resources on your behalf. It only notifies you as each resource is downloaded. However, you need to take some action once all resources have loaded.

My approach to these problems was a common one: use a system I call  delayed preemptive dispatch (the term “dispatch” coming from Apple’s Grand Central Dispatch (GCD) libdispatch library available on all their operating systems from the Watch to the TV).

The gist of this system is I want to perform a task after a brief delay, but only if I don’t preempt it with another task before the prior task’s timer expires (in effect canceling the prior one); it is at the conclusion of the last submitted delay that the last submitted task is executed.

Here’s how I apply this to the above three problems, identified by the event I wish to be notified of:

  1. Invalid passcode expired. I start a one-minute timer when a passcode has been wrongly entered that, upon expiration, runs a task to reset the entry count. However, I don’t want that closure to run if, in the meantime, the user has re-entered the passcode. Instead, I want to preempt the first closure, replacing it with a second one-minute timer that will itself reset the entry count. Importantly, once the user enters the passcode correctly, I want to cancel the task altogether.
  2. Panning stopped. Here I start a timer upon receipt of a pan-stopped notification that, upon expiration, declares all-stop on panning. However, I don’t want that to occur if a subsequent pan-stop event occurs in the meantime. Rather, I want to preempt the preceding closure with the next and, yes, initiate a new delayed notification closure.
  3. All resources loaded. Finally, in this case I start a brief timer after each resource loads that, upon expiration, signals all resources loaded. However, I don’t want that signal to fire if a subsequent resource loads before the timer expires. Instead, I want to preempt the preceding notification closure and replace it with another one that, if not itself preempted, will be the one to notify all resources have been loaded.

Implementation

At this GitHub repo you will find an implementation of what I call a DelayedPreemptiveDispatchQueue, written in Swift 3, which includes unit tests and the above-illustrated UI demo.

With this class you can delay execution of a task until expiration of a timer, with the option to later reset the timer, preempt the task with another, or cancel the task altogether. It makes use of the Swift 3 GCD DispatchGroup to manage concurrent timers and fire off the delayed task once all the timers have expired.

Here’s a peek at the delay(_:task:) method in that class (see the repo for the full source):

/// Executes `task` no earlier than after the given delay. 
///
/// Subsequent calls to this method will preempt execution of this `task`. The task may also be provided via the class initializer. Cancel execution of a task via `cancel()`.
///
/// - parameter seconds: The number of seconds before this or the initializer-givn task may be executed. The delay will be longer if there is a pending delay that expires after this period.
/// - parameter task: The closure to run after _all_ pending delays have expired. If `nil` (the default), the task to be run is that given at initialization. If no task was given during initialization or at any call to this method, it takes no action.
func delay(_ seconds: TimeInterval, task: (() -> Void)? = nil) {

  // Make sure we have a defined task task
  if task != nil {
    self.task = task
  }
  guard self.task != nil else { return }

  // Newly submitted task preempts prior cancellation
  cancelled = false

  // Add delay timer to the dispatch group
  group.enter()
  self.queue.asyncAfter(deadline: DispatchTime.now() + seconds) {
    self.group.leave()
  }

  // Only assign GCD notify action when first timer added to group.
  // The notify action, which performs the task, runs once all timers have left the group.
  if empty {
    self.empty = false
    group.notify(queue: .main) {
      if !self.cancelled {
        self.task()
      }
      self.empty = true
    }
  }
}

The task may be provided via init(label:task:) (not shown), delay(_:task:), or a combination of both. Neither the delay times nor tasks need be the same with each call. Only the last task submitted will be called once all concurrent delay timers have expired. While you cannot cancel the timers themselves, you can cancel execution of the task via cancel() (also not shown). Once all timers have expired, you can reuse the DelayedPreemptiveDispatchQueue with or without specifying a new task.

While this class makes use of the DispatchGroup to manage a group of timers, the functionality given by DelayedPreemptiveDispatchQueue is not inherent to it. For example, you cannot provide a task to a DispatchGroup at initialization, but only via its instance notify() method. Also, all tasks submitted to a dispatch group execute; there is no concept of “preemption” as there is in this class.

For documentation on the Swift 3 implementation of Grand Central Dispatch, reference this current pre-release Swift 3 evolution proposal 88, “Modernize libdispatch for Swift 3 naming conventions.”

Leave a Comment