How does QOS Propagation work in iOS?

Vaibhav Singh
5 min readMay 24, 2021

--

This article aims to develop an understanding of the quality of service classes, and how using them we can appropriately direct resources to the most important tasks in a multi-threaded environment.

A higher priority task often uses more energy, as it may require substantial and immediate access to system resources. We can use the QOS classes to let the system know where it needs to allocate resources, stuff that impacts the user directly like UI updates will take precedence and should be given a higher priority, the system then can allocate more resources to it to try to finish it off as quickly as possible.

Brief description of different types of QOS

  • User-interactive — This is stuff done on the main thread, the focus is more on performance and responsiveness. All UI-related stuff, updating the user interface, or running animations require this QOS in order to focus all energy on these actions so the app experience is smooth.
  • User-initiated — This is used when the user has performed an action and is waiting on results, like for eg. opening a document or tapping on a button. This requires performance and responsiveness as these actions are needed to continue user interaction.
  • Utility — These tasks require a while to complete, for example downloading images. They usually have some sort of progress bar to communicate the progress to the user. There is a balance between energy efficiency and performance for these tasks.
  • Background — These are tasks invisible to the user like synchronizing, indexing, etc. Focus is more on energy efficiency there

There are 2 more special QOS classes:

  • Default: This lies between user-initiated and utility. This class is not supposed to be used by developers to classify work. Work that doesn’t have a QOS assigned is assigned this. The global queues run on this QOS.
  • Unspecified: The absence of QOS, it tells the system that an environment QOS should be inferred. This can happen if the thread is using legacy APIs which can opt of QoS entirely

Assigning QOS to DispatchWorkItem

init(qos:flags:block:) : DispatchWorkItem init takes in a QOS(default value of unspecified), a param called DispatchWorkItemFlags, and a block to execute the work.

DispatchWorkItemFlags

These define a set of behaviors for a work item, like QOS, creating a new detached thread or a barrier.

Possible value for the flags

  • assignCurrentContext: This makes the work item take in the QOS from the current context i.e from the dispatch queue or the thread it runs on.
  • detached: this flag dissociates the current context from the work item
  • enforceQOS: this flag prioritizes the queue’s QOS over the block’s QOS as long as it doesn’t lower the QOS.
  • barrier: work item submitted with this flag acts as a barrier, all tasks prior to it will finish, and then it will start executing alone. Till it finishes no other task will execute even if we queue them.
  • inheritQOS: this flag prioritizes the QOS of the context over that of the work item as long as the QOS doesn’t get lowered.
  • noQOS: execute the work with any QOS, this takes precedence over assignCurrentContext

QOS propagation

inheritQoS

Say an async task is submitted to a dispatch queue(without a specified QOS) from the main thread. Then user interactive automatically propagates to user-initiated so we don’t overpropagate the QOS reserved for the main thread. When coming back to the main thread the automatic QOS will try to take place again, but since the main queue has a user interactive QOS, QOS is not lowered and we ignore the propagated value.

So inferred QOS is the QOS captured at the time block gets submitted to the queue. The rule here says user-interactive is propagated to user-initiated. The propagated QOS is used if the destination of the block doesn’t have a specified QOS. The last rule is QOS is not lowered when we move to something like the main thread.

Block QOS

Let’s take an example of a long-running job here, we have asynchronously dispatched a long-running task from the main thread and provide continuous progress updates to the main thread.

We can assign a QOS of utility to a work item’s init as mentioned above. This happens at the point where the QOS changes, so any work further downstream will be able to use this QOS class through automatic propagation for example any async work from the block will occur at QOS utility.

Queue QOS

Now let’s consider an example where we need to perform some maintenance work on the background. The user is unaware of this work. We can use a block QOS like above, but in situations like this where a task has its own purpose and can be started from different parts of the app, it’s better to assign a queue a QOS, for example in this case background QOS.

When we dispatch a task to that queue asynchronously, the automatic propagation will convert user-interactive to user-initiated, but since the queue has an assigned QOS, we ignore the propagated value

We can use the detached DispatchWorkItemFlag while creating the block, as this work is detached from the current flow of the app. This will help to opt-out of automatic propagation of QOS and will discard the other properties of the execution context.

EnforceQOS

Extending the example above, say we need to override the behavior for a certain use case. Like a logout and deletion on a database happen on a background queue, but the user may want to see that happening to make sure his data is deleted. So this task’s automatic propagation from the main thread will give us user-initiated QOS which is what we need. In this case, we can use a DispatchWorkItemFlag called enforce QoS, which will ignore the queue QOS and use the propagated value of user-initiated.

Async Priority Inversion

Say in a serial queue we dispatch two blocks will different QOS. If a higher QOS block is added, the QOS is raised till the higher QOS block is reached, this promotion is invisible to the block, the block when propagated further will use its assigned QOS. GCD does this priority inversion to make sure the higher QOS tasks are done as soon as possible.

I hope this article was helpful in explaining how QOS classes can be used with dispatch queues and work items to maximize the system’s resources. I am planning to cover the use of QOS classes with operations and operation queues in a separate article. Thanks for reading!

References

  • Building Responsive and Efficient Apps with GCD — WWDC 2015
  • Apple’s documentation

--

--

Vaibhav Singh

iOS Engineer @ Pulselive Leading development of the Premier League App!