Chaining a Series of NSOperations

In Part 1, we looked at the basics of the NSOperation class and how to build our own. Today, we’ll taking things to the next level: we’ll look at how operations can work together in order to create new, complex behaviors.

Trust me: once you get familiar with this, you’ll see the uses everywhere.

Let’s have a look at the classic example of downloading and parsing data. We’ll start with 2 operations: one that downloads Data from a URL, and another that parses Data into a given model object. We want to achieve something like this:

Dependency Chain

As things stand, we have 2 challenges we need to overcome.

  • First of all, we need to make sure that the DownloadOperation executes before the ParseOperation. This is obvious, but not quite straightforward. These operations are asynchronous, running on background threads. How can we guarantee that they’ll execute in the correct order?
  • Secondly, we need to pass the downloaded Data from our DownloadOperation to our ParseOperation. Nothing exists to do this out of the box, so we’ll need to roll our own solution.

Let’s get started!

Dependencies — How to precisely control when your operations will execute

In the last article, we glossed over the Ready state of the NSOperation, but now it’s time to go more in depth. When an operation is Ready, it means that all of its dependencies have completed.

But wait a sec Frank, what do you mean by dependencies?

If operation B depends on operation A, the system guarantees that operation B will never execute before operation A. Regardless of the fact that operations are asynchronous, we can still reason about their timing and execution.

Dependencies also work between different operation queues and threads. Thanks Apple! 🙌🏻

What’s more, creating a dependency between two operations is extremely easy. It’s as simple as this:

operationB.addDependency(operationA)

So this is how we get over our first hurdle: if we make our ParseOperation depend on our DownloadOperation, we no longer longer need to worry about them executing out of order. Great!

Adapters - How to pass data from one operation to another

At the moment we have 2 separate operations. I rather not combine them, since they each make sense on their own. A DownloadOperation could download data that isn’t parsed (like a video), and a ParseOperation could be used to parse data that isn’t downloaded (like a file on disk).

But now we need to get these operations to talk to each other. There are a few ways we could go about this, but my favourite is using a third operation: the adapter operation.

An adapter operation is simply a BlockOperation that takes data from one operation and passes it to the other. These are single-use operations that we can build on the fly when we need them.

It’ll be sandwiched between our 2 operations. That means we need to change how our dependencies are handled. Instead of having our ParseOperation depend on our DownloadOperation, we’ll have it depend on our adapter operation. Also, our adapter operation will depend on our DownloadOperation. It’ll look something like this:

Dependencies with adapter op

This effectively creates a chain of operations that you know will execute in the right order. Here’s what the code would look like:

let downloadOp = DownloadOperation()
let parseOp = ParseOperation()
let adapterOp = BlockOperation() {
    parseOp.data = downloadOp.data 
} 

parseOp.addDependency(adapterOp)
adapterOp.addDependency(downloadOp)

queue.addOperations([downloadOp, adapterOp, parseOp])

So what’s going on here exactly? When our DownloadOperation finishes, the adapter operation will have all of its dependencies satisfied. Then it’ll execute, taking the data from the DownloadOperation and setting it on the ParseOperation. Once that’s done, the adapter operation will finish and the ParseOperation will have its dependencies satisfied. It’ll execute and parse the data that was passed to it.

Pretty cool, n’est-ce pas?

Watch out for retain cycles and deadlocks

There’s one last thing we need to add to our example above, because as things stand, we have a nasty memory leak going on. To avoid this, we need to add a capture list to our adapter operation to weakly capture the references to the other operations. If we don’t, none of our operations will ever deallocate.

let adapterOp = BlockOperation() {
    [unowned downloadOp, unowned parseOp] in
    parseOp.data = downloadOp.data
}

I’d like to extend my thanks to Nick Harris for figuring this out! You can find his article about it here: Retain Cycle with NSOperation Dependencies (His blog has plenty of amazing resources on NSOperations in Swift. I highly recommend poking around and reading it!)

Also, a quick word of warning. If you make 2 operations dependant on each other (i.e. operation A depends on B, and B depends on A), neither of them will execute. This is a deadlock, where each operation is waiting for the other to finish.

Operation Deadlock

Feel the power!

There you have it! You’re now able to create Operations and chain them together.

Though as you can imagine, you’ll often be downloading and parsing together. Next time, we’ll look at how we can wrap commonly used operations together so we can keep our code nice and DRY.