Separate serial and concurrent DispatchQueue by type

I want to inject a DispatchQueue which must be serial when creating an object in construtor.
However, it is impossible due to serial and concurrent DispatchQueue are not separated by type.
If serial and concurrent DispatchQueue are separated by type - for example, SerialDispatchQueue and ConcurrentDispatchQueue - it can be possible to restrict which implementation to use.

1 Like

You can subclass DispatchQueue but there's no way to make the existing initializers unavailable, so you should still initialize a SerialQueue subclass configured concurrently. So it's possible, but you can't guarantee the behavior in the type system. Additionally, there's no way to check whether a DispatchQueue instance is configured serially or concurrently at runtime, so there's really now way to ensure the behavior you want, unfortunately.

1 Like

Thank you for your answer, but I already knew that it is not possible now.
I think it's a good idea to implement this by subclassing is one of the best ways to do it.
Why not introduce this into Swift code rather than having individual developers implement it?

A technique I learned from RxSwift is to create a serial dispatch queue that targets the queue of unknown type.

Yup, this is the right pattern: if you need a serial queue, then accept an unknown queue as an argument and create a private serial queue that targets the one given as an argument. This will enforce serial queue semantics, even if the user passes a concurrent queue.

4 Likes

Seems that is approach breaks dispatchPrecondition(condition: .onQueue) when dealing with multiple layers of targeting, which is unfortunate.

It should not.

import Foundation
import Dispatch

extension DispatchQueue {
    func createSubQueue() -> DispatchQueue {
        return DispatchQueue(label: "sub-queue", target: self)
    }
}

func main() {
    let topQueue = DispatchQueue(label: "temporary")
    let nestedQueue = topQueue.createSubQueue().createSubQueue().createSubQueue()
    nestedQueue.async {
        dispatchPrecondition(condition: .onQueue(topQueue))
        print("passed precondition")
        exit(0)
    }

    dispatchMain()
}

main()

prints "passed precondition" and exits cleanly.

Right, the check works top down, but not middle up. This is roughly equivalent to what I was doing:

import Foundation
import Dispatch

extension DispatchQueue {
    func createSubQueue() -> DispatchQueue {
        return DispatchQueue(label: "sub-queue", target: self)
    }
}

func main() {
    let topQueue = DispatchQueue(label: "temporary", attributes: .concurrent)
    let middle = topQueue.createSubQueue()
    let nestedQueue = middle.createSubQueue().createSubQueue()
    middle.async {
        dispatchPrecondition(condition: .onQueue(nestedQueue))
        print("passed precondition")
        exit(0)
    }

    dispatchMain()
}

main()

Despite ultimately targeting the same queue, middle and nestedQueue are not considered the same. Thankfully I can remove one additional layer of targeting.

I would say this is absolutely correct. Dispatch queue targets are trees. Being on a parent queue does not mean you are on a child of that parent.

1 Like

If nestedQueue can be said to be on topQueue, and middle can be said to be on topQueue, it seems logical that being on middle would be equivalent to being on nestedQueue since they're both equivalent to topQueue. But I understand if the structure can't actually work that way. I had to remove an instance where I'd like to use targeting, but I think I was able to fix at least one other dangerous case with this technique.

I think this is a conceptual boundary: being on a queue doesn't mean that queue X is equivalent to queue Y. A really good example of this is Dispatch specific variables. These are associated with a specific queue. If you have set a dispatch specific variable on nestedQueue, I think you'd be fairly surprised to find that it was present on middle, let alone on topQueue.

Another example would be to think of this as a question of substitutability: a child queue is substitutable for its parent (being on the child queue means you are definitionally on the parent), but the reverse is not true.

I meant for the purposes of the onQueue check only, not that the queues would be identical.

You can achieve that by wrapping dispatch queue in another type (class or struct), like below. obviously you'll need to do queue.queue when you want to reach out to the underlying queue, or wrap those API's like "async", etc calls as well in your wrapper to keep the main app code base concise.

struct SerialDispatchQueue {
    let queue: DispatchQueue

    init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil) {
        precondition(!attributes.contains(.concurrent), "don't specify concurrent flag when creating a serial queue")
        var attributes = attributes
        attributes.remove(.concurrent)
        queue = DispatchQueue(label: label, qos: qos, attributes: attributes, autoreleaseFrequency: autoreleaseFrequency, target: target)
    }
}

struct ConcurrentDispatchQueue {
    let queue: DispatchQueue

    init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [.concurrent], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil) {
        precondition(attributes.contains(.concurrent), "do specify concurrent flag when creating a concurrent queue")
        var attributes = attributes
        attributes.insert(.concurrent)
        queue = DispatchQueue(label: label, qos: qos, attributes: attributes, autoreleaseFrequency: autoreleaseFrequency, target: target)
    }
}

func wantsSerial(_ queue: SerialDispatchQueue) {
    ...
    queue.queue.async { ... }
    // could be just:
    queue.async { ... }
    // if you reimplement `async` in your wrapper
}

func wantsConcurrent(_ queue: ConcurrentDispatchQueue) {
    // ...
}

func test() {
    wantsSerial(SerialDispatchQueue(label: "world")) // ok
    wantsSerial(ConcurrentDispatchQueue(label: "hello"))
    // Error: Cannot convert value of type 'ConcurrentDispatchQueue' to expected argument type 'SerialDispatchQueue'
}