iOS Concurrency with GCD & Operations

Sep 12 2023 · Swift 5.8, macOS 13, iOS 16, Xcode 14.3

Part 3: Operations & OperationQueues

16. OperationQueues

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 15. Operations Next episode: 17. Challenge: TiltShiftOperations in OperationQueue

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 16. OperationQueues

In this video you’ll discover that the real power of Operations is when you add them to an OperationQueue. Instead of starting the operation yourself, you’ll hand it over to an operation queue and let it handle scheduling and execution.

The OperationQueue default initializer creates an operation queue, so that part is easy. Then you can set its properties and add operations to it. There are two class properties: the current operation queue and the main operation queue, which is where you should do all UI updates, just like the GCD main queue. defaultMaxConcurrentOperationCount is -1, which means “let the system decide”. Or you can set the maxConcurrentOperationCount property to specify a limit. Setting it to 1 creates a serial operation queue.

You can create an operation as a closure or as a subclass of the operation class, then add it to the operation queue. Or you can add an operation directly to the operation queue, as a closure, like the simplest dispatch queue task.

You can create an array of operations, either separately or inline, to add several operations at once, specifying whether you want the current thread to wait synchronously for all of these operations to finish. When you’ve created operations for all the tasks you need in your app, you let OperationQueue manage their execution.

The operation queue executes operations that are ready, roughly first-in-first-out, allowing for quality of service values and dependencies. An operation leaves the queue when it finishes or is cancelled. Once you’ve added an operation to a queue, you cannot add the same operation to any other operation queue. An operation object is a single-shot object, that is, it executes its task once and cannot be used to execute it again. This is why it’s good to create operation subclasses, so you can instantiate a new operation when you need one.

You can send a cancel message to all operations in the queue, for example, when the view or app is about to become inactive. And you can block the current thread until the operation queue operations finish, but don’t do this on the main thread!

If you want to do something when all the operations finish, set up a private serial dispatch queue, where you can call the operation queue’s waitUntilAllOperationsAreFinished or set the waitUntilFinished argument of addOperations to true, and add your completion steps.

OperationQueue actually behaves like a DispatchGroup: You can add operations with different quality of service values, and they’ll run according to the corresponding priority. You can also set the quality of service level for the entire queue, but the quality of service level of specific operations can override this. The default quality of service level of an operation queue is background.

You can pause a queue with the isSuspended property: Currently executing operations continue, but the queue won’t start any others. You can continue to add operations to a suspended queue, but it won’t schedule them until you change isSuspended to false. You can monitor this value with KVO. Its default value is false.

Before you add any operations to an operation queue, you can specify an existing dispatch queue (not main!) as its underlyingQueue. The underlyingQueue‘s quality of service level overrides any value set for the operation queue’s quality of service. In the next exercise, you’ll get some hands-on experience with operation queues.

An Operation, by itself, is like a synchronous task, which you could dispatch asynchronously to a dispatch queue, to move it off the main thread. But to gain the full concurrency benefits of operations, you add Operations to OperationQueues.

In the starter playground, create an empty OperationQueue:

let printerQueue = OperationQueue() // pause at ( to show initializer

There’s only the default initializer. Scroll down to see the same 5 operations that were in the multiPrinter BlockOperation in the previous video, wrapped in a duration closure. Uncomment these 7 lines:

duration {
  printerQueue.addOperation { print("Hello"); sleep(3) }
  printerQueue.addOperation { print("my"); sleep(3) }
  printerQueue.addOperation { print("name"); sleep(3) }
  printerQueue.addOperation { print("is"); sleep(3) }
  printerQueue.addOperation { print("Audrey"); sleep(3) }
}

Here’s the first difference between BlockOperation and OperationQueue: you don’t need to start an OperationQueue. Just as you don’t need to start a dispatch queue. When you add an operation to an operation queue, it runs as soon as it’s ready. So run the playground and open the debug area:

(in side pane: duration 0.0001649856567382812)
Hello
my
name
is
Audrey

As with asynchronous dispatch to a dispatch queue, adding operations took very little time, but the print messages took longer to appear. This shows that the operation queue sent the operations off to run on background threads.

Now find out how long the operations take to complete:

duration {
  printerQueue.waitUntilAllOperationsAreFinished()
}

You’re calling the synchronous waitUntilAllOperationsAreFinished method: Like DispatchGroup’s wait method, this blocks the current thread, so you must be careful using it in an app. But it’s safe to use it here, where nothing else is happening. Run the playground:

duration 3.006882905960083 <in side pane> 

And look, it took 3 seconds, so the operations all ran at the same time! Now specify the maxConcurrentOperationCount property, up here when you first create the operation queue:

printerQueue.maxConcurrentOperationCount = 2

Setting this to 2, you’d expect it will take longer to complete the 5 operations. Run the playground:

duration 9.008874893188477 <in side pane> 

And in fact, it runs 2 at a time, so it takes 9 seconds. So that’s a simple example, to show the basics of how OperationQueues work. Next up is a challenge where you get to use an operation queue to filter an array of images.