Discovering which DispatchQueue a method is running on

Discovering which DispatchQueue a method is running on

The DispatchQueue class has lots of interesting and undocumented (unfortunately) hidden gems. Let's see how we can use one of these gems to get a method's current DispatchQueue -- a trick that can also be used to send and retrieve information from a DispatchQueue.

Why is this useful?

When you have a specific method that can be executed from multiple queues, determining which queue it is running on can be useful. The reason I personally find this most useful for is performance: Imagine that you have a piece of code like this:

func doSomething() {
    queue.async { ... }
}

Dispatching code isn't very cheap, and if your queue has a low priority, there's no guarantee that it will be executed instantly. If doSomething is already running in the correct queue, you can detect this and avoid re-dispatching your code. For example, you can use the tricks from this article to prevent calling DispatchQueue.main.async unnecessarily when you need to update an UI element in the main queue. If you're already in the main queue, you can directly execute that piece of UI-updating code.

Additionally, dispatching anything asynchronously can make you lose your stack trace. If something crashes inside an async queue, you might not be able to be see trace that precedes the execution of the dispatch queue as it might already be out of scope (after all, the code was dispatched asynchronously!). By avoiding unnecessary dispatches, you can have richer stack traces that will make debugging code and crashes easier.

Lastly, as mentioned, you can also use this trick to change how your method works depending on which queue it's running on. Different queues can have different requirements -- especially lower priority ones. A low priority background thread can, for example, disable some logging features to make it run quicker.

Determining if a method is running on a specific DispatchQueue

Checking if a method is running on a specific queue can be done through a hidden gem: The getSpecific and setSpecific methods. DispatchQueues support holding a user info dictionary that behaves similarly to a UserDefaults, which can be retrieved globally without having access to the queue itself. You can use this to "tag" a queue and later determine if a method is running on it by checking if this tag exists in the method's current queue.

To add data to a DispatchQueue's dictionary, create a DispatchSpecificKey<T> instance that matches the type that you want to store and add to the queue by calling setSpecific.

For example, here's how we can add an arbitrary string value a the queue:

let queue = DispatchQueue(label: "SwiftRocks")

let specificKey = DispatchSpecificKey<String>()
let valueToStore = "myValue"

queue.setSpecific(key: specificKey, value: valueToStore)

Now, from any method, you can check if it's running on this specific "SwiftRocks" queue by checking the result of getSpecific:

func doSomething() {
    if DispatchQueue.getSpecific(key: specificKey) == specificValue {
        print("Running on a thread from the 'SwiftRocks' queue.")
    } else {
        print("Not running on a thread from the 'SwiftRocks' queue.")
    }
}

doSomething() // Not running on a thread from the 'SwiftRocks' queue.
queue.sync { doSomething() } // Running on a thread from the 'SwiftRocks' queue.

Passing information to a DispatchQueue

Storing multiple values that have the same type is slightly trickier, but doable. This is easy in the equivalent Objective-C dispatch_queue_set_specific method as the key value can be anything (just like a regular dictionary), but for some reason, in Swift this was bridged to the immutable DispatchSpecificKey type.

Since everything is handled by reference, you can store multiple values of the same by creating different instances of DispatchSpecificKey. The downside is that you'll have to make sure that your methods can access the keys.

let recommendedNetworkTimeout = DispatchSpecificKey<Int>()
queue.setSpecific(key: recommendedNetworkTimeout, value: 30)

let recommendedRetryAmount = DispatchSpecificKey<Int>()
queue.setSpecific(key: recommendedRetryAmount, value: 3)

queue.sync {
    DispatchQueue.getSpecific(key: recommendedNetworkTimeout)
    DispatchQueue.getSpecific(key: recommendedRetryAmount)
}

This is very useful if you need to create a set of "rules" that a method should follow that differs depending on the queue that it is running on. For example, like in the previous snippet, a background queue with a lower priority can have a shorter network retry amount than a user-facing one, and as another example, we could disable logging features for queues that are performance-critical:

public let loggingQueueKey = DispatchSpecificKey<Bool>()

extension DispatchQueue {
    public static var allowsLogging: Bool {
        return getSpecific(key: loggingQueueKey) ?? true
    }

    public func disableLogging() {
        setSpecific(key: loggingQueueKey, value: false)
    }

    public static func log(_ block: () -> Void) {
        guard Self.allowsLogging else {
            return
        }
        block()
    }
}

queue.disableLogging()

func reloadContent() {
    DispatchQueue.log { print("Reloading content") }
    viewModel.reload()
    DispatchQueue.log { print("Content reloaded") }
}

reloadContent() // Prints
queue.sync { reloadContent() } // Doesn't print

This will allow you to have more control over your code, making choices that have performance benefits depending on a method's context, which leads to happier users.