`TaskPriority` of task group's child tasks

I've recently been trying out new things with TaskGroups but I've hit a point with child task's priority argument where I am not sure if it is a bug or just something that is to be expected.

Consider this example:

Task(priority: .high) {
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask(priority: .low) {
            print("child", "base", Task.basePriority!, "current", Task.currentPriority)
        }
        print("parent", "base", Task.basePriority!, "current", Task.currentPriority)
    }
}

This is the output:

parent base TaskPriority.high current TaskPriority.high
child base TaskPriority.low current TaskPriority.high

Even though the child tasks priority was set to .low it will print .high as its current priority.

A couple of thoughts that I had what could explain this:

  • This is due to privilege escalation similar to Task.value. However it is not. If you switch the priorities, the child task will still inherit the parent's task priority, but then it would be de-escalation (?).
  • Task.currentPriority simply ignores the priority of a task groups child task. This would be weird, because Task.basePriority returns the correct base priority.

It feels like the parent Task's priority will always override the priority of the child task. Am I missing something?

Thanks.

I’m not sure what you mean by “if you switch the priorities”. There’s no de-escalation.

Escalation happens because the group waits on the child and bumps it.

Can you give an example of an unexpected situation?

I meant that there is no observable difference if the parent is .low and child is .high or parent is .high and child is .low.

This:

Task(priority: .low) {
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask(priority: .high) {
            print("child", "base", Task.basePriority!, "current", Task.currentPriority)
        }
        print("parent", "base", Task.basePriority!, "current", Task.currentPriority)
    }
}

Prints this:

parent base TaskPriority.low current TaskPriority.low
child base TaskPriority.high current TaskPriority.low

And this:

Task(priority: .high) {
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask(priority: .low) {
            print("child", "base", Task.basePriority!, "current", Task.currentPriority)
        }
        print("parent", "base", Task.basePriority!, "current", Task.currentPriority)
    }
}

Prints this:

parent base TaskPriority.high current TaskPriority.high
child base TaskPriority.low current TaskPriority.high

I mean the unexpected situation is pretty much the code I posted. The priority of addTask was set to .low but in the closure it is observed to be .high. I have no concrete example of problems that are occurring, I was just expecting something different.

i think one of the questions here is whether the priority inversion avoidance behaviors are expected to be reflected in the value that Task.currentPriority reports in situations where one might expect priority escalation to occur. in the given example, it seems that if a parent Task has a higher priority than a child Task added via a TaskGroup, then the child's priority will be increased to meet its parent's. however, in the situation in which the parent has a lower priority than the child, as highlighted, it doesn't appear that the higher-priority child 'boosts' the priority of the parent, at least as reported by Task.currentPriority.

one possible explanation is that the priority escalation isn't 'visible' via these mechanisms, but some cursory investigation using a debugger and taskinfo also suggests the relevant thread's QoS values aren't increased in the latter case. furthermore, there seems to be something of a behavioral difference b/w a low priority Task awaiting a high priority child Task added via the TaskGroup API, and a low priority Task awaiting a high priority unstructured Task. e.g.

  Task.detached(priority: .low) {
    print("parent", "base", Task.basePriority!, "current", Task.currentPriority)

    let t = Task(priority: .high) {
      for i in 1...Int.max {
        if i % 1_000_000 == 0 {
          print("child", "base", Task.basePriority!, "current", Task.currentPriority)
        }
      }
    }

    _ = await t.result
  }
// prints:
// parent base TaskPriority.low current TaskPriority.low
// child base TaskPriority.high current TaskPriority.high
// child base TaskPriority.high current TaskPriority.high

  Task.detached(priority: .low) {
    print("parent", "base", Task.basePriority!, "current", Task.currentPriority)

    await withTaskGroup(of: Void.self) { taskGroup in
      taskGroup.addTask(priority: .high) {
        for i in 1...Int.max {
          if i % 1_000_000 == 0 {
            print("child", "base", Task.basePriority!, "current", Task.currentPriority)
          }
        }
      }
    }
  }
// prints:
// parent base TaskPriority.low current TaskPriority.low
// child base TaskPriority.high current TaskPriority.low
// child base TaskPriority.high current TaskPriority.low

also, if you empirically try to estimate the throughput of these two configurations, the one which does not use a TaskGroup appears to get much more execution time (at least initially... maybe they converge if run for long enough).

perhaps that's all to say... there are some interesting questions here, and it's not really clear to me how this system is intended to work.

2 Likes

Circling back to this, and why I even brought up this topic in the first place: Assuming that the problem is not the observability of Task.currentPriority and TaskGroup does always inherit its parent priority, like @jamieQ and me have outlined in those experiments, why does TaskGroup.addTask even take priority as parameter?