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