frank
frank

Reputation: 2404

DispatchQueue.global() default qos is userInitiated?

I write a demo

let queue = DispatchQueue.global()
queue.async {
    let group = DispatchGroup()
    group.enter()
    DispatchQueue.global().asyncAfter(deadline: DispatchTime.now(), qos: .default) {
        //do sth in default queue
        group.leave()
    }
    group.wait()
    DispatchQueue.main.async { [weak self] in
        //do sth in main queue
    }
}

then I get a thread performance check warning

Thread running at QOS_CLASS_USER_INITIATED waiting on a lower QoS thread running at QOS_CLASS_DEFAULT. Investigate ways to avoid priority inversions

and I try to print outer queue's qos it shows

(lldb) print queue.qos
(Dispatch.DispatchQoS) $R0 = {
  qosClass = unspecified   //I think this should be default.
  relativePriority = 0
}

the apple doc is wrong?

class func global(qos: DispatchQoS.QoSClass = .default) -> DispatchQueue

Upvotes: 2

Views: 2864

Answers (1)

Rob
Rob

Reputation: 438487

The title of your question says,

DispatchQueue.global() default qos is userInitiated

Consider:

examineQoS(qos: .default)         // original QoS: default;          queue QoS: DispatchQoS(qosClass: .unspecified,     relativePriority: 0); thread.QoS: .userInitiated   !!! default global queue is `.unspecified` and thread is `.userInitiated`
examineQoS(qos: .userInteractive) // original QoS: userInteractive;  queue QoS: DispatchQoS(qosClass: .userInteractive, relativePriority: 0); thread.QoS: .userInteractive
examineQoS(qos: .userInitiated)   // original QoS: userInitiated;    queue QoS: DispatchQoS(qosClass: .userInitiated,   relativePriority: 0); thread.QoS: .userInitiated
examineQoS(qos: .utility)         // original QoS: utility;          queue QoS: DispatchQoS(qosClass: .utility,         relativePriority: 0); thread.QoS: .utility
examineQoS(qos: .background)      // original QoS: background;       queue QoS: DispatchQoS(qosClass: .background,      relativePriority: 0); thread.QoS: .background

func examineQoS(qos: DispatchQoS.QoSClass) {
    let queue = DispatchQueue.global(qos: qos)
    queue.async {
        print("original QoS:", qos, "; queue QoS:", queue.qos, "; thread.QoS:", Thread.current.qualityOfService)
    }
}

extension QualityOfService: CustomStringConvertible {
    public var description: String {
        switch self {
        case .userInteractive: return ".userInteractive"
        case .userInitiated:   return ".userInitiated"
        case .utility:         return ".utility"
        case .background:      return ".background"
        case .default:         return ".default"
        @unknown default:      return "@unknown default"
        }
    }
}

Note that global() (or global(qos: .default)) is actually creating a queue with a QoS of .unspecified, and therefore its worker threads will inherit the QoS of the caller (i.e., the main queue, .userInitiated).


OK, let’s do the same logging in your code snippet:

let outerQueue = DispatchQueue.global(qos: .default)
outerQueue.async {
    print("outerQueue:", outerQueue.qos, "; thread:", Thread.current.qualityOfService)

    // outerQueue: DispatchQoS(qosClass: .unspecified, relativePriority: 0) ; thread: .userInitiated

    let group = DispatchGroup()
    group.enter()
    let innerQueue = DispatchQueue.global()
    innerQueue.asyncAfter(deadline: DispatchTime.now(), qos: .default) {
        print("innerQueue:", innerQueue.qos, "; thread:", Thread.current.qualityOfService)

        // innerQueue: DispatchQoS(qosClass: .unspecified, relativePriority: 0) ; thread: .default

        //do sth in default queue
        group.leave()
    }
    group.wait()
    DispatchQueue.main.async { [weak self] in
        self?.doSomething()
    }
}

So, the outer queue (the one dispatched from the main queue) grabbed a worker thread of QoS .userInitiated, but the inner queue (the one dispatched from the outer queue) used a worker thread of QoS .default (because you explicitly specified .default in the asyncAfter call). So the “user-initiated” thread is waiting for a “default” thread: A priority inversion.


This begs the question of how does one avoid this priority inversion.

  1. Obviously, one could just specify an explicit QoS for the two queues, ensuring that you do not have a high QoS thread waiting for a lower QoS one.

  2. You could remove the qos parameter from asyncAfter, and the inner queue would inherit the QoS from the caller and there would be no priority inversion.

  3. The deeper observation is that one should avoid calling wait, where possible. It is inefficient and ties up one of the GCD worker threads (which are quite limited). And if you ever did it from the main queue, it could have quite serious implications.

    In short, rather than wait, one should notify:

    let outerQueue = DispatchQueue.global(qos: .default)
    outerQueue.async {
        let group = DispatchGroup()
        group.enter()
        DispatchQueue.global().asyncAfter(deadline: .now()) {
            //do sth in default queue
            group.leave()
        }
        group.notify(queue: .main) { [weak self] in   // NB: not `wait`
            self?.doSomething()
        }
    }
    

You go on to ask:

the apple doc is wrong?

class func global(qos: DispatchQoS.QoSClass = .default) -> DispatchQueue

Technically, the source of the confusion is not the default value the qos parameter of global(qos:). Even if one explicitly specifies a QoS of .default, that queue’s underlying qos class is still “unspecified”, as shown above.

You said:

I think this should be “default”.

I am sympathetic to this opinion, but rather than having the default global queue with a fixed QoS in between “user-initiated” and “utility”, it may be a conscious decision to have the default global queue have an “unspecified” QoS, i.e., have it avail itself of worker threads of a QoS appropriate to the calling context. E.g., consider:

DispatchQueue(label: "Background", qos: .background).async {
    DispatchQueue.global().async {
        // `Thread.current.qualityOfService` is background; that makes sense
    }
}

There is a chance that this default/unspecified global queue behavior is a conscious decision to avoid unnecessary priority inversions on this global queue in the standard use-case. This may be a designed “feature”, not a “bug”. (Note, this seems to be limited to the default QoS global queue; our own default QoS custom queues do not manifest this behavior.) Pouring through the libDispatch source code, it is hard to divine whether this behavior was intended (as there is no code that I am seeing that explicitly enforced this pattern) or whether it is just a side effect (intended or not).

But I agree with you. It would be more correct if:

  1. The default parameter for the global queue was .unspecified (given that none was specified; lol); and
  2. If you specify an explicit QoS of .default, it should actually be .default, not .unspecified.

I have opened a ticket to that end (though I am not confident that it will be accepted, given that correcting this behavior is not entirely backwards compatible ... at least they could clarify in the documentation).


The legacy Energy Efficiency Guide for iOS Apps doc describes default and unspecified QoS as follows:

Default

The priority level of this QoS falls between user-initiated and utility. This QoS is not intended to be used by developers to classify work. Work that has no QoS information assigned is treated as default, and the GCD global queue runs at this level.

Unspecified

This represents the absence of QoS information and cues the system that an environmental QoS should be inferred. Threads can have an unspecified QoS if they use legacy APIs that may opt the thread out of QoS.

Upvotes: 8

Related Questions