Leave a rating/review
Notes: 12. Explore Priority Inversion
In this exercise, you’ll use a semaphore and queues with different quality of service values to create a priority inversion: A low priority queue gets a lock on a resource, so a high priority queue has to wait until the resource is free. In the starter playground, create global queues with higher and lower qos values than the medium queue, and a semaphore with value 1:
let high = DispatchQueue.global(qos: .userInteractive)
let medium = DispatchQueue.global(qos: .userInitiated) // existing
let low = DispatchQueue.global(qos: .background)
let semaphore = DispatchSemaphore(value: 1)
The semaphore controls access to some resource needed by tasks on the high and low priority queues. Now dispatch a task to the high priority queue:
high.async {
semaphore.wait()
defer { semaphore.signal() }
print("High priority task is now running")
sleep(1)
PlaygroundPage.current.finishExecution()
}
You’re setting up this playground so the high-priority task finishes last, so you stop the playground when it finishes. The 1-second sleep gives the print statement time to appear.
In the TODO below the medium priority for-loop, dispatch a task to the low priority queue:
low.async {
semaphore.wait()
defer { semaphore.signal() }
print("Low priority task is now running")
}
Now, create the conditions for priority inversion: Make the high priority task sleep before semaphore.wait:
high.async {
sleep(2)
print("High priority task is now waiting")
semaphore.wait() // existing
defer { semaphore.signal() } // existing
print("High priority task is now running") // existing
PlaygroundPage.current.finishExecution() // existing
}
… and make the low priority task sleep after it starts running:
low.async {
semaphore.wait() // existing
defer { semaphore.signal() } // existing
print("Low priority task is now running") // existing
sleep(5)
}
You’re simulating a situation where the high priority task does some work before it needs the resource that semaphore protects, while the low priority task takes a long time to finish using that resource. A 2-second sleep gives all the other tasks time to get in the queue before the high priority task requests the resource.
The medium priority tasks don’t use the resource at all:
for i in 1 ... 10 {
medium.async {
print("Running medium task \(i)")
let waitTime = Double(Int.random(in: 0..<7))
Thread.sleep(forTimeInterval: waitTime)
}
}
Run the playground several times: You might see a different order of execution each time.
Running medium task 2
Running medium task 1
Running medium task 3
Running medium task 4
Running medium task 5
Running medium task 6
Running medium task 7
Running medium task 9
Running medium task 8
Low priority task is now running
Running medium task 10
High priority task is now waiting
High priority task is now running
Sometimes the lowest priority task sneaks in before some of the medium priority tasks. But the high priority task waiting message always appears after the low priority task starts. And the high priority task always starts after all the other tasks.
Priority inversion is an especially difficult problem to identify, so Apple’s concurrency frameworks solve it by promoting lower priority tasks in a queue to the same level as the high priority task. The high priority task doesn’t jump the queue, but the lower priority tasks finish sooner, so don’t slow it down as much.
This solution to the priority inversion problem can have surprising side effects. When you create a private queue, you can specify a quality of service qos value, or the system will give it the same qos as the queue where you create it. And the qos value sets the priority of its threads. When you dispatch a higher-priority task onto it, the dispatch system will promote it to that qos level.
The diagram shows the main queue and a private queue with qos utility.
When you dispatch a task from the main queue onto this private utility queue, the default qos of that task is user-initiated, so the system will promote your utility queue to that higher qos level. All the tasks that really should run at utility level will now have the same priority as tasks dispatched from the UI. They’ll compete directly with them for running time, and could prevent genuine user-initiated tasks from running as soon as they should.
You need to keep this in mind, to prevent Apple’s priority inversion solution from creating a priority inversion problem.
DispatchWorkItems can help you avoid this side effect: You could create a work item in a task running on your utility queue, and use its assignCurrentContext flag to capture the utility qos level. Then you can dispatch it from anywhere, including the main queue, to a utility queue, and it won’t promote that queue’s priority.
There’s a link below to Apple’s rules for Quality of Service inference and promotion.
The next topic is data races and how to make a class thread-safe with dispatch barriers.