In the introduction of this series, I mentioned that Grand Central Dispatch operates at the system level. It has an overview of the processes running on the system and the resources that are available. When your application submits a block of work to a dispatch queue, it's up to Grand Central Dispatch to decide when that block of work is executed.
The system manages a wide range of resources and decides which processes have access to those resources and at what time. If your application is frontmost, then the system tries to provide the required resources for the execution of the application's main thread. Remember that the main thread is responsible for two key tasks, (1) handling user interface events and (2) drawing the application's user interface. Tasks that aren't critical shouldn't be performed on the main thread.
What happens if your application runs on a device with limited capabilities or constrained resources? Take energy as an example. If the device is connected to a power outlet, then the system can optimize for performance at the cost of efficiency. If the device is running low on energy or the user has enabled Low Power Mode on their device, then the system optimizes for efficiency at the cost of performance. It does this by scaling back the number of operations running concurrently.
This brings us to a key question. How can an application inform the system about the importance of a task? Or how can a developer communicate its intent to the system? Quality of service classes are the answer to these questions and they're the focus of this episode.
What Are Quality of Service Classes?
Quality of services classes explicitly classify the work that is submitted to Grand Central Dispatch. This has several advantages, one of them being clarity. By assigning a quality of service class to a block of work, the developer communicates its intent to the system through a single, abstract parameter. The words single and abstract are key.
The developer uses a single parameter. There's no need to define a range of configuration options. Setting the quality of service class of a block of work or a dispatch queue suffices.
The quality of service classes add a layer of abstraction. The developer doesn't need to be aware of the device or the platform their application is running on. It doesn't matter whether the application runs on a powerful desktop computer running macOS or an Apple Watch running watchOS. The developer doesn't and shouldn't need to know about the capabilities or resources of the device the application runs on. That is something the system handles.
The goal of quality of service classes is to resolve resource contention. This may seem less of an issue if you consider the power of modern devices, but that's a common misconception. It's true that modern devices are incredibly capable, but the expectations and requirements have also evolved over the years. Take iPad as an example. A few years ago, Apple added the ability to run applications side by side. This has a number of important implications if you consider that every application has a main thread that needs to stay responsive. Running applications side by side is only possible if the system carefully controls the available resources and understands exactly what the needs of the various processes running on the system are. To manage that complexity, Grand Central Dispatch defines quality of service classes.
A single, abstract parameter classifies the work a process, such as your application, performs. The quality of service class of a task gives the system the information it needs to schedule it for execution. Let's explore the quality of service classes you can use.
Quality of Service Classes
Every task that is submitted for execution has a quality of service class assigned to it, implicitly or explicitly. From the perspective of the developer, a quality of service class defines the intent of the developer. From the perspective of the system, a quality of service class defines the priority of the block of work. Let's make these statements more concrete by listing the quality of service classes Grand Central Dispatch defines.
User Interactive
The user interactive quality of service class informs Grand Central Dispatch about a critical piece of information, the work that is scheduled is directly related to the user experience of the application and should be executed as soon as possible.
If the user is scrolling a table or collection view, then it's vital that any work related to this interaction is classified as user interactive to guarantee a responsive and performant application. Any work that is performed on the main thread automatically receives the user interactive quality of service class.
It goes without saying that the amount of work performed on the main thread needs to be limited. Apply the user interactive quality of service class sparingly. Classifying non-critical work as user interactive won't improve the performance of your application.
User Initiated
Work classified as user initiated is less important than work classified as user interactive. As the name implies, this type of work is the result of the user interacting with the application. The user initiated quality of service class communicates to the system that the user performed an action and is waiting for a response.
Consider the following example. The user taps a row in a table view to open a document that is stored on the device. We don't want to open the document on the main thread because that operation may take a non-trivial amount of time to complete. It could result in the application becoming unresponsive if performed on the main thread. The solution is simple. We dispatch the work to a background thread, but, to ensure that its execution is prioritized, we assign it the user initiated quality of service class. This informs Grand Central Dispatch that the user is waiting for the work to complete and it should prioritize its execution.
Utility
The utility quality of service class is a perfect fit for work that is directly or indirectly triggered by the user, but they aren't waiting for the results of their action. This quality of service class is aimed at tasks that are long running and visible to the user. Downloading content that isn't immediately relevant to the user is an example of such work.
Consider the following example. You're building an application to play podcasts and it offers the ability to download audio files for offline use. Downloading data in the background isn't a priority for the user, but they are aware of the task and they can monitor the progress of the download tasks. This type of work is ideal for the utility quality of service class.
Background
The background quality of service class is probably the easiest to understand. If an application performs a task in the background that the user is unaware of, then the background quality of service class is the right choice. Maintenance tasks, indexing operations, and data synchronization are examples of such tasks.
Default
When a block of work or a dispatch queue doesn't have specific quality of service information, then its quality of service class is equal to default. Remember from the previous episodes that we haven't explicitly defined the quality of service class of some of the work the application submitted to Grand Central Dispatch. Those blocks of work have a quality of service class that is equal to default.
The default quality of service class is a placeholder. Grand Central Dispatch tries to infer the quality of service class that best fits the block of work or it assigns a quality of service class based on the context and available resources. The default quality of service class sits in between the user initiated and utility quality of service classes. Keep in mind that the default quality of service class isn't meant to be used explicitly. You should never assign the default quality of service class to a block of work or a dispatch queue.
Unspecified
When a block of work has no quality of service class assigned to it, then the quality of service class is set to unspecified. This quality of service class indicates that there's no quality of service information available. Grand Central Dispatch attempts to infer the quality of service class by inspecting the origin of the block of work, such as the thread it was submitted from.
Quality of Service Classes in Practice
Grand Central Dispatch defines several APIs for specifying the quality of service class. Let's revisit the options we encountered earlier in this series. Fire up Xcode and create a playground by choosing the Blank template from the iOS > Playground section. Replace the contents of the playground with an import statement for the Foundation framework.
import Foundation
Dispatch Queues
Earlier in this series, we used the global dispatch queues Grand Central Dispatch creates automatically for every application. The global() class method of the DispatchQueue class accepts one optional parameter of type DispatchQoS.QoSClass.
The DispatchQoS struct encapsulates the information associated with a quality of service class. It defines two properties. The first property, qosClass, is of type DispatchQoS.QoSClass and defines the quality of service class. The second property, relativePriority, is of type Int and defines the priority of the DispatchQoS instance relative to other instances with the same quality of service class. Don't worry about this property for now.
Let's ask Grand Central Dispatch for the global dispatch queue with quality of service class userInitiated. This should look familiar by now.
import Foundation
let dispatchQueue = DispatchQueue.global(qos: .userInitiated)
Any block of work that is submitted to the dispatch queue automatically receives a quality of service class of userInitiated. It inherits the quality of service class of the dispatch queue it is submitted to.
import Foundation
let dispatchQueue = DispatchQueue.global(qos: .userInitiated)
dispatchQueue.async {
print("some work")
}
The syntax is simple and lightweight thanks to the static properties defined by the DispatchQoS struct. It defines a static property for each quality of service class. DispatchQoS.QoSClass is an enum that defines the six quality of service classes we discussed earlier in this episode.
public enum QoSClass {
@available(OSX 10.10, iOS 8.0, *)
case background
@available(OSX 10.10, iOS 8.0, *)
case utility
@available(OSX 10.10, iOS 8.0, *)
case `default`
@available(OSX 10.10, iOS 8.0, *)
case userInitiated
@available(OSX 10.10, iOS 8.0, *)
case userInteractive
case unspecified
@available(OSX 10.10, iOS 8.0, *)
public init?(rawValue: qos_class_t)
@available(OSX 10.10, iOS 8.0, *)
public var rawValue: qos_class_t { get }
}
Dispatch Work Items
Earlier in this series, we worked with the DispatchWorkItem class. A dispatch work item gives us more control and flexibility. It allows us to define a block of work and carefully control when and how it's executed by Grand Central Dispatch. Remember that the initializer allows us to explicitly set the quality of service class of the dispatch work item.
let workItem = DispatchWorkItem(qos: .utility) {
print("some work")
}
The initializer also accepts one or more flags to define the behavior of the dispatch work item. Some of these flags impact the quality of service class of the dispatch work item. We revisit these flags later.
let workItem = DispatchWorkItem(qos: .utility, flags: [.inheritQoS]) {
print("some work")
}
Operations and Operation Queues
The Operation and OperationQueue classes have been around since the early days of Cocoa development. With the introduction of quality of service classes in iOS 8 and macOS 10.10, Apple redesigned the Operation and OperationQueue classes to take advantage of Grand Central Dispatch. The Operation and OperationQueue classes are built on top of Grand Central Dispatch, which implies that any work an application performs using operations and operation queues is subject to quality of service classes.
Operations and operation queues are in some ways similar to blocks of work and dispatch queues. For example, it's possible to set the quality of service class of an operation queue by setting its qualityOfService property.
let operationQueue = OperationQueue()
operationQueue.qualityOfService = .background
You also have the option to set the quality of service class of an operation through its qualityOfService property. If both the operation queue and the operation define a quality of service class, then the quality of service class of the operation takes precedence.
// Initialize Operation Queue
let operationQueue = OperationQueue()
// Set Quality of Service Class
operationQueue.qualityOfService = .background
// Initialize Operation
let operation = BlockOperation {
print("some work")
}
// Set Quality of Service Class
operation.qualityOfService = .utility
// Add Operation to Operation Queue
operationQueue.addOperation(operation)
Threads and Processes
Threads and processes are also subject to quality of service classes. Quality of service classes are only effective if every component of the system participates. I won't cover threads and processes in detail in this series, but we briefly revisit threads in the next episode.
// Initialize Thread
let thread = Thread()
// Set Quality of Service Class
thread.qualityOfService = .background
Assigning a quality of service class to a process may seem odd, but it makes sense if you consider daemons and command line applications. It can be useful in the context of performance and efficiency to ensure a background process operates in the background by assigning it the background quality of service class.
What's Next?
I hope that this episode has illustrated the importance of quality of service classes because they are an integral component of Grand Central Dispatch. This aspect of Grand Central Dispatch is often overlooked or ignored by developers. Don't make that mistake.
We only covered the fundamentals of quality of service classes in this episode. In the next episode, we continue our exploration. Quality of service classes are a fascinating topic and there are a few more details you must understand to properly apply them in your projects.