I'm a software developer based in the UK. I am blogging regularly about software development & Apple

QoS with Dispatch Queues

A mobile system needs to weigh various factors against each other to achieve the best performance. What is the best performance? Is it good speedy app performance, overall system performance or excellent battery performance. So achieving the "best performance" is not an easy task; let alone the sheer amount of variables that have to be considered. But to make it easy for the developer who might want to participate in the decision making, the system must not overwhelm him with complexity of the underlying task. That’s where Quality of Service (QoS) classes come in.

Types of Classes

As you can see in the picture above there are 5 different classes:

  • User Interactive
    All UI related tasks need to be assigned that QoS class. It basically tells iOS to run them on the main thread.
  • User Initiated
    This is the class to choose if a task is triggered by the user but doesn't have to be run necessarily on the main thread.
  • Default
    This class is only listed for informational purposes only. The system defaults to this class when not enough information is available to determine QoS.
  • Utility
    This class needs to be chosen for tasks that have a dependency on other system resources like file I/O or network I/O. Those tasks need to wait for a certain amount of time before they are able to complete. They are always in one way or another initiated by the user who waits for their completion.
  • Background
    This class is supposed to be used for maintenance tasks - Tasks that don't depend on their fast execution and that are oblivious to the user.

Each task assigned to a QoS can be fine-tuned via its relative priority (a negative value between 0 and -15). Hence as the picture above implies there is a smooth transition from one service class to the next.

So where can that new Quality of Service be used? Well, it can be used with processes, threads, dispatch queues, dispatch blocks and since the NSOperation API sits on top of those GCD classes, you can use it with NSOperationQueues and NSOperations as well. Having said that, let's take a look at how to work with QoS and GCD.

QoS with Dispatch Queues & Blocks

In the following I would like to take a look at the various ways of enqueuing blocks in GCD and explain the implications of the used method. Let's create a queue first with QoS Utility

let attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_UTILITY, 0)
let q = dispatch_queue_create("utility queue", attr)

and now let's add a block

dispatch_async(q) {
    // Do something
}

This works of course as before (i.e. pre iOS8), but blocks created that way will be created with the flag DISPATCH_BLOCK_INHERIT_QOS_CLASS set. This means that the block will be assigned the priority of the queue. In other words, its equivalent to

let block1 = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {
    // Do something
}
dispatch_async(q, block1)

Let's create block a block with a different QoS class.

let block2 = dispatch_block_create_with_qos_class(DISPATCH_BLOCK_INHERIT_QOS_CLASS, QOS_CLASS_USER_INITIATED, 0)
{ // Do something }
dispatch_async(q, block2)

Mind that the queue has a lower QoS class than the block. But because of the block's flag DISPATCH_BLOCK_INHERIT_QOS_CLASS it will be assigned the queue's priority when added. Let's change that flag parameter and see what happens:

let block3 = dispatch_block_create_with_qos_class(DISPATCH_BLOCK_ENFORCE_QOS_CLASS, QOS_CLASS_USER_INITIATED, 0)
{ // Do something }
dispatch_async(q, block3)

Now the block will keep its priority class. This has further implications: If there are other blocks on the queue that have a lower priority we'd run into a priority inversion. To mitigate this issue the queue's QoS will be set to block3's QoS until block3 has been completed provided that block3's QoS is higher than the queue's. If not then the block's QoS will be set to the queue's QoS.

If you are explicit in your queue and block creation like in those samples above things of what's going to happen are quite clear. If you fall back to simpler pre iOS8 API methods iOS is going to infer the QoS class. There is handful of rules around class inference. So if you like it short and concise I recommend to memorize the rules for this. Since I prefer it more descriptive, I'm rather explicit when working with QoS. Let's move on and look at two more examples.

let block4 = dispatch_block_create_with_qos_class(DISPATCH_BLOCK_DETACHED, QOS_CLASS_USER_INITIATED, 0)
{ // Do something }
dispatch_async(q, block4)

The flag DISPATCH_BLOCK_DETACHED tells iOS that the block has nothing to do with the current context and that there is no need to infer its QoS. Hence the block will be assigned the QoS of its target queue. This happens as well when a block is enqueued with dispatch_after():

let block5 = dispatch_block_create(DISPATCH_BLOCK_ENFORCE_QOS_CLASS) {
    // Do something
}
dispatch_after(dispatch_time_t(2),q,block5)

Conclusion

The concept of QoS is not hard to grasp. Especially if you are already used to working with queue priorities before iOS 8. Those old queue priorities, still available but deprecated, match by the way one-to-one to the new QoS classes:

As mentioned above the tricky bit is where QoS class inference comes in. It's not always quite clear to me which QoS will be applied. While playing around with the API in the playground, I wished having a method like dispatch_queue_get_qos_class() for queues that returns the current QoS for a given dispatch block. Unfortunately Apple didn't provide one and I don't have high hopes for one to show up in iOS 10. But let's see.

 

 

Asynchronous Core Data Requests

Stackviews in iOS9