In the docs for TaskPriority
I saw that Swift tries to help prevent priority inversion by upgrading a low priority task when it is awaited in a high priority task. I was even able to confirm this behavior:
Task(priority: .high) {
print("outer", Task.currentPriority.rawValue)
await Task(priority: .low) {
print("inner", Task.currentPriority.rawValue)
}.value
}
This prints:
outer 25
inner 25
…even though the inner task is of low priority.
Whereas if the outer task is low and the inner task is high, then the inner task just stays as high:
Task(priority: .low) {
print("outer", Task.currentPriority.rawValue)
await Task(priority: .low) {
print("inner", Task.currentPriority.rawValue)
}.value
}
outer 17
inner 25
So clearly the inner task is upgraded to a high priority even though it was created as low priority.
But then I wondered how this works with task groups and async let
, and I'm not sure I understand the results.
Task groups seem to ignore the child priority and always use the parent priority. So, if the outer task is low and the child task is high, both seem to be low:
Task(priority: .low) {
print("outer", Task.currentPriority.rawValue)
await withTaskGroup(of: Void.self) { group in
group.addTask(priority: .high) {
print("inner", Task.currentPriority.rawValue)
}
}
}
outer 17
inner 17
That doesn't seem correct based on my reading of SE-304. Or perhaps Task.currentPriority
doesn't mean what I think it should mean in the context of a child task of a task group?
And then for async let
, it does work as I expect in this simple set up:
func perform() async {
print("inner", Task.currentPriority.rawValue)
}
Task(priority: .high) {
print("outer", Task.currentPriority.rawValue)
async let inner: Void = perform()
}
That prints:
outer 25
inner 25
…and the inner and outer always print the same no matter what the parent priority is.
But things get weird if I wrap the operation in a Task
with a different priority and then async let
it:
func perform() async {
print("inner", Task.currentPriority.rawValue)
}
Task(priority: .high) {
print("outer", Task.currentPriority.rawValue)
let task = Task(priority: .low) {
await perform()
}
async let inner: Void = task.value
}
This prints:
outer 25
inner 17
But I would expect both to print 25 since the inner task should have its priority elevated to high. So this seems like a possible vector for priority inversion since a high priority outer task is waiting for a low priority inner task.
Even stranger, if I make the inner task .background
, then it does elevate the priority:
func perform() async {
print("inner", Task.currentPriority.rawValue)
}
Task(priority: .high) {
print("outer", Task.currentPriority.rawValue)
let task = Task(priority: .low) {
await perform()
}
async let inner: Void = task.value
}
outer 25
inner 25
Anyone have insight into what is going on here? Are some of these issues Swift bugs?