GCD Part 5: DispatchSource and Target Queue Hierarchy
In this article, we will discuss some niche concepts such as DispatchSource and target queue hierarchy.
DispatchSource
DispatchSource is the fundamental type that handles system events. It can be an event listener for different types like file system events, signals, memory warnings and etc.
I don’t think we often use this construction in daily work but in some cases, it’s important to be aware of it especially if you work with low-level functionality. Further, we will look through some subtypes you can find useful in your apps.
We will start with the most known Dispatch source - DispatchSourceTimer. As you can guess by its name it works like a simple timer and generates periodical notifications which you can process in the event handler. Here is a simple example in the code snippet below:
let timerSource = DispatchSource.makeTimerSource()func testTimerDispatchSource() {
timerSource.setEventHandler {
print(“test”)
}
timerSource.schedule(deadline: .now(), repeating: 5)
timerSource.resume()
}
It prints the word “test” in the console every 5 seconds.
Important: You should keep the reference to the data source somewhere in your code otherwise it will be deallocated and you will not be able to catch events.
DispatchSourceMemory will help to handle memory issues you can encounter during the application work time. It can be useful if you want to have a central place in your architecture that logs memory issues. In the example below, it shows how you can listen for the memory warnings. You can simulate memory warning in the simulator using Debug -> Simulate Memory Warning.
let memorySource = DispatchSource.makeMemoryPressureSource(eventMask: .warning, queue: .main)func testMemoryDispatchSource() {
memorySource.setEventHandler {
print(“test”)
}
memorySource.resume()
}
DispatchSourceSignal will track all the UNIX signals sent to the application. That can be useful if you are developing a console application. In the example below we are catching the SIGSTOP signal. To emulate that one you can press the Pause button and then Resume in the debugger panel in Xcode.
let signalSource = DispatchSource.makeSignalSource(signal: SIGSTOP, queue: .main)func testSignalSource() {
signalSource.setEventHandler {
print(“test”)
}
signalSource.resume()
}
Using DispatchSourceProcess we can listen to other processes for receiving signals or making forks. For example, you can use it to monitor other processes in the non-iOS application. All the events you can find in DispatchSource.ProcessEvent. In the example below we will listen to our process of receiving signals similar to what we did in the previous example. ProcessInfo.processInfo.processIdentifier returns processId of the current process.
let processSource = DispatchSource.makeProcessSource(identifier: ProcessInfo.processInfo.processIdentifier, eventMask: .signal, queue: .main)func testProcessSource() {
processSource.setEventHandler {
print(“test”)
}
processSource.resume()
}
As you can see the syntax of the event handling looks identically in all the examples and I hope it can provide you with some gasp on how and when you can use DispatchSource inside your applications.
Important: Do not forget to call the method cancel or suspend after you finish using DispatchSource.
It was a quick review of different DispatchSource subtypes and how to work with them. To find out how to work with DispatchSourceFileSystemObject I can recommend you to go through this article.
Target queue hierarchy
That is an important concept to understand. Let’s say we have multiple queues in the app. We can redirect the execution of their tasks to one specific queue which is called the target queue. In the example below you can see 4 serial queues: 1 target queue and 3 others where the target queue is specified. To check that all the tasks are executing on the same target queue we will print current thread information. As you can see the thread is the same in all the cases. The target queue has utility QoS and it means that all the tasks executing on it will not have QoS less than the utility. Indeed, we can see the queue which has background QoS is executing on utility instead. The queue which doesn’t have a QoS will be executed on userInitiated because we are creating it from the main queue so it acquires userInteractive and decreases to userInitiated according to QoS rules. Learn more about the QoS you can here.
let targetQueue = DispatchQueue(label: “com.test.targetQueue”, qos: .utility)let queue1 = DispatchQueue(label: “com.test.queue1”, target: targetQueue)let queue2 = DispatchQueue(label: “com.test.queue2”, qos: .background, target: targetQueue)let queue3 = DispatchQueue(label: “com.test.queue3”, qos: .userInteractive, target: targetQueue)targetQueue.async {
print(DispatchQoS.QoSClass(rawValue: qos_class_self()) ?? .unspecified)
print(Thread.current)
}queue1.async {
print(DispatchQoS.QoSClass(rawValue: qos_class_self()) ?? .unspecified) print(Thread.current)
}queue2.async {
print(DispatchQoS.QoSClass(rawValue: qos_class_self()) ?? .unspecified) print(Thread.current)
}queue3.async {
print(DispatchQoS.QoSClass(rawValue: qos_class_self()) ?? .unspecified)
print(Thread.current)
}
Result:
utility
<NSThread: 0x600003ec6600>{number = 6, name = (null)}userInitiated
<NSThread: 0x600003ec6600>{number = 6, name = (null)}utility
<NSThread: 0x600003ec6600>{number = 6, name = (null)}userInteractive
<NSThread: 0x600003ec6600>{number = 6, name = (null)}
All the tasks which will be enqueued to queue1, queue2, and queue3 will be executed in the target queue. If we would not use the target queue we can encounter a situation when thread explosion could occur because each serial queue executes the task on its own thread and that can produce massive context switching. So the target queue is preventing this scenario.
Based on that idea Apple recommends we use one target queue per subsystem which ofc very reasonable: having a small number of serial queues (and respectively threads) is more efficient than having a lot working in parallel.
Today we learned different types of DispatchSource and how to work with the target queue hierarchy.