Asynchronous

Operation: Queue

A couple of weeks ago, I shared some tips on how to use GCD queues.  This week I wanted to talk about two classes OperationQueue and their friend Operation.

OperationQueue

OperationQueue is sort of a higher-level version of GCD queues.  While they share many similar features (such as quality of service indicators, etc), OperationQueues offer some great additional functionality.  Firstly, OperationQueues allow you to cancel the queue.  This will stop all items that in the queue from running and end those that are currently running. Secondly, you can easily add items into the queue (Operations).

One of the difference between OperationQueue and GCD is that just because you specify a certain quality of service, doesn’t mean you’ll get it.  Like most higher level items, you are sacrificing control for convenience.

Operation

Operations are bits of work that you add to a queue.  The Operation class is itself an abstract class and so needs to be subclassed before you can use it. However there are two subclasses provided by Apple (BlockOperation and InvocationOperation).  The nice things about the Operation class (and its children) are glimpsed via its properties, isCancelled, isReady, isFinished, dependencies, to name a few.  From these properties we see that an operation may be cancelled, dependent (on other operations), or notify when ready or finished.

Example

While the info above is exciting, its not super useful without a concrete example.  For that I’m going to borrow from David DeLong’s wonderful 2015 WWDC prevention on Operations.  In it he outlines how the WWDC app uses Operations and OperationQueues to perform some of its functions. In his example he states that if a user wants to save a movie to favorites, a number of operations are dependent and must be executed prior to the saving operation.  Here is the diagram from the presentation:

https://developer.apple.com/videos/play/wwdc2015/226/

From the above diagram, we can see that saving a video to favorites depends on the completion of five other operations (News, Feedback, Favorites, Videos, Beacons) which are in turn dependent on the completion of other operations.  So how would this look in code? First lets start with the creation of a OperationQueue:

// Init
let queue = OperationQueue.init() // use OperationQueue.main to get the main queue

// Set Qos
queue.qualityOfService = .userInitiated

Pretty simple. Now lets start by creating the Settings and Version operations.  For this we will just utilize the BlockOperation subclass of Operation:

// Init Settings and give it some actions
let settings = BlockOperation {
    print("check settings")
    sleep(1)
}

settings.completionBlock = { print("check settings completed")}

// Init Version and give it some actions
let version = BlockOperation{
    print("check version")
    sleep(1)
}
version.completionBlock = { print("check version completed")}

// make version dependent on settings
version.addDependency(settings)

Not too bad. I’ve added the sleep methods to make it seem like something that actually takes some time is happening.

Finally, we need to add the operations to the queue:

self.queue.addOperations([settings, version], waitUntilFinished: true)

Now, if you were to run this code in say, a view controllers viewDidLoad method, you would see the following output:

Check Settings
Check Settings completed
Check Version
Check Version completed

Snazzy! But what about the rest of the operations? Well, first off, I’m not a big fan of typing a ton. A good programmer is driven by laziness, and so does all that he can, not to type (or cmd-c, cmd-p) things twice. Lets subclass BlockOperation to help us out a bit:

class BlockOperationSublcass: BlockOperation{
    
    convenience init(message:String, sleepTime:UInt32, dependancies:[Operation]? = nil) {
        self.init()
        self.qualityOfService = .background
        self.addExecutionBlock {
                print(message)
                sleep(sleepTime)

        }
        self.completionBlock = {
            if !self.isCancelled {
                print(message + " completed")
            }else{
                print(message + " was cancelled")
            }
        }
        if let unwrappedDepndancies = dependancies {
            for dependancy in unwrappedDepndancies {
                self.addDependency(dependancy)
            }
        }
    }
    // If you don't override cancel, when you cancel, nothing will happen.
    override func cancel() {
        super.cancel()
    }
}

Now we can create our operations in one line:

let settings = BlockOperationSublcass(message: "Check Settings", sleepTime: 1)
let version = BlockOperationSublcass(message: "Check Version", sleepTime: 1, dependancies: [settings])

let news = BlockOperationSublcass(message: "Get News", sleepTime: 1, dependancies: [version])
let sessions = BlockOperationSublcass(message: "Get Sessions", sleepTime: 1, dependancies: [version])
let beacons = BlockOperationSublcass(message: "Get Beacons", sleepTime: 1, dependancies: [version])

let feedback = BlockOperationSublcass(message: "Get Feedback", sleepTime: 1, dependancies: [sessions])
let favorites = BlockOperationSublcass(message: "Get Favorites", sleepTime: 1, dependancies: [sessions])
let videos = BlockOperationSublcass(message: "Get Videos", sleepTime: 1, dependancies: [sessions])

let save = BlockOperationSublcass(message: "Save", sleepTime: 1, dependancies: [news, beacons, favorites, feedback, videos])

You can clearly se what is going on here.  Save is dependent on the five operations listed above, who are in turn dependent on the operations as diagramed above. We can add them to the queue like so:

self.queue.addOperations([settings, version, news, sessions, beacons, feedback, favorites, videos, save], waitUntilFinished: true)

Running this we get:

Check Settings
Check Settings completed
Check Version
Check Version completed
Get News
Get News completed
Get Sessions
Get Sessions completed
Get Beacons
Get Beacons completed
Get Feedback
Get Feedback completed
Get Favorites
Get Favorites completed
Get Videos
Get Videos completed
Save
Save completed

Beautiful! Now.  What if a user accidentally saves and quickly cancels, before the saving is done? No problem! Simply tell the queue to cancel and all will work out as you’d hoped!

While the above code is certainly overly simplistic (you most likely will not be adding everything to the queue in one go, but rather little by little as needed), it does illustrate the powerful features associated with OperationQueue and Operation.

TL;DR

OperationQueues are high-level versions of GCD that provide powerful features such as cancel and operation additions while handling the nitty gritty of thread usage (such as which thread to use, when to run operations, etc).

Operations are packets of work added to an OperationQueue. The class itself is abstract, but BlockOperation and InvocationOpeartion are two ready made subclasses.

let queue = OperationQueue.init()
let blockOperation = BlockOperation {
    // Do work
    
}
//blockOperation.addDependency(dependantBlock)

queue.qualityOfService = .userInitiated
queue.addOperation(blockOperation)

 

Sources

https://developer.apple.com/videos/play/wwdc2015/226/

https://developer.apple.com/documentation/foundation/operation#main

https://developer.apple.com/documentation/foundation/operationqueue

http://www.cimgf.com/2008/02/16/cocoa-tutorial-nsoperation-and-nsoperationqueue/

http://nshipster.com/nsoperation/

 

Asynchronous

GCD (No, not that type of GCD)

So, you’ve got this great app that does some pretty awesome stuff.  However, some of that stuff takes time. Perhaps its a network call, complex calculation of image manipulation, potentially time consuming cache clearing. Whatever it may be, you’ve noticed that your application is starting to lag or becomes completely unresponsive.  Shoot! What can we do about that?

GCD to the rescue!

GCD stands for Grand Central Dispatch. What a name! (It always makes me think of Grand Central Station). GCD was first released with OS X Snow Leopard for the purpose of solving the issues we mentioned above.  You see, it was when Snow Leopard was released, that computers started having multiple cores.  To take advantage of this, Apple produced GCD with enables developers to send tasks to the different cores on different threads.

The Basics

To allow a developer to access the different threads, GCD provides a dispatch queue called (ready for this?) DisptachQueue.  Here is the down low on DispatchQueue’s:

  • Dispatch queue’s manages tasks you send them in a FIFO (first in, first out) order.
  • Can be either serial (tasks finish sequently, 1,2,3…) or Concurrent (task can finish at any time (1,3,2,5,4…). Note they all start in a FIFO order, but serial queue’s only start the next task once the current one is finished.
  •  There are three types of DispatchQueue’s:
    1.  Main. This runs on the main thread and is a serial queue. This is why things can lag if the main queue has too much work on it.
    2. Global queue’s.  These are concurrent queue’s which the OS shares with your application. These come in four priority types: high, default, low, background. Note: you don’t set the priority directly, instead you use the Quality of Service (QoS) property.  More on this in a second.
    3. Custom queue’s. These are either serial or concurrent and are user created. Eventually, these are handled by a global queue.
QoS

In order for the system to properly allocate threads and save on energy, there are a lot of properties you can set on a queue. These properties take a lot of tuning in order to achieve the desired/expected outcome.  In order to simplify this for developers, apple has released some base classes you can set on a queue which will provide the correct settings for these properties.  These are the Quality of Service classes. There are four of them:

  1. User-Interactive. This class is used when you are doing work that needs to be done immediately for a good user experience, like UI updates and such.. The main thread is a User-interactive thread (hence why you do all your UI updates on the main thread.)
  2. User-Initiated. For doing work which the user has started from the UI and is waiting for immediate results or for tasks that require completion for the user to continue. These are marked as a high priority global queue.
  3. Utility. For long running computations. These types of queue’s often are accompanied by a progress indicator. E.G: computations, network calls, large image manipulation, etc.
  4. Background. These are things that need to be done, but the user doesn’t need to know or worry about them.  Clearing up cache and prefetching are two such examples.
Lets Try it out!

Now that we know all this stuff thats go through some examples. Lets say we have a network call where we download an image or sets of images. Lets add an extension to UIImageView to do this:

extension UIImageViwe{
  public func imageFromServerURL(urlString: String, completionBlock:(()->())? = nil) {
        guard let URL = URL(string: urlString) else {return}
        do {
            let data = try Data.init(contentsOf: URL)
            let image = UIImage(data: data)
            self.image = image
        } catch {
            print("Crap I had an error");
        }
    }
}

If you do it this way, your app is libel to lag, as all the work is being done on the main thread.  Particularly if you have a slower internet connection. To fix this code, you can use dispatch queues:

extension UIImageView {

    public func imageFromServerURL(urlString: String, completionBlock:(()->())? = nil) {
        guard let URL = URL(string: urlString) else {return}
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                let data = try Data.init(contentsOf: URL)
                let image = UIImage(data: data)
                DispatchQueue.main.async {
                    self.image = image
                }
            } catch {
                print("Crap I had an error");
            }
        }   
    }    
}

Now if you use this in say, a UITableViewCell that is duplicated many times, scrolling is snappy and pleasant!

Quick note: Dispatch queue propagate their QoS, meaning that if you create a dispatch queue and don’t specify the QoS it automatically applies the current QoS of the queue you are in.  There are two exceptions to this.  First, if launching from a UserInteractive, the default QoS is a user initiated.  This is done to not tie up the main thread accidentally. Secondly if moving from a lower thread (say background) to a thread that is higher in priority (say UserInitiated or UserInteractive) the higher QoS is applied.

This means that we could have simplified the above code by not specifying global(qos: .userInitiated), but just used global(), since we were on the main thread, global() would have created a thread with a UserInteractive QoS.

While the above code is OK for an example and teaching purposes, it would be better to use URLSession to fetch the image:

extension UIImageView {
    public func imageFromServerURL(urlString: String, completionBlock:(()->())? = nil) {
        guard let URL = URL(string: urlString) else {return}
        URLSession.shared.dataTask(with: URL , completionHandler: { (data, response, error) -> Void in
            
            if let unwrappedError = error {
                #if DEBUG
                    print(unwrappedError)
                #endif
                return
            }
            guard let unwrappedData = data else {return}
            DispatchQueue.main.async(execute: { () -> Void in
                let image = UIImage(data: unwrappedData)
                self.image = image
                if let unwrappedBlock = completionBlock {
                    unwrappedBlock()
                }
            })
        }).resume()
    }
}

Here is another example that I’ve needed to do.  One application I wrote (a sort of Instagram for hunters) would cache the images viewed by the user.  This was great for speeding up view times in various places of the app, however we quickly noticed that the app size ballooned to multiple gigabits. To combat this we decided to clear the cache every day or so.

DispatchQueue.global(qos: .background).async {
    //Clear cache
}

Note we use the background QoS.

TL;DR
/*
 QOS Types:
 .default - propogates current QoS (unless moving to higher or is UserInteractive
 
 .userInteractive - for UI work
 .userInitiated - for work needed to be done ASAP
 .utility - for long running, progress updating tasks
 .background - for tasks the user doesn't need to worry about
 
*/
DispatchQueue.global(qos: .default).async {
    // Do your work here
    
    // update the UI:
    DispatchQueue.main.async {
        // UI updates go here
    }
}

Readings and Examples:

Grand Central Dispatch Tutorial for Swift 3: Part 1/2

https://developer.apple.com/videos/play/wwdc2015/718/

https://developer.apple.com/videos/play/wwdc2016/720/