Multithreading/QOS for Timer

254 Views Asked by At

For timer related operations is it worth using any QOS? If yes then which thread should be used or which QOS can we use?

Now here I am creating a Timer and adding it to Runloop:

var activityTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.runActivity), userInfo: nil, repeats: true)

RunLoop.current.add(activityTimer!, forMode: .common)

I have created global object of DispatchQueue with QOS .userInteractive:

let queue = DispatchQueue(label: "com.example.timer", qos: .userInteractive)

Here I am sending data to API:

@objc func runActivity () {
    queue.async {
        self.postDataUsingAPI()
    }
}

So which QOS should be appropriate to use here? I want all these activities running in the background only.

2

There are 2 best solutions below

7
matt On

I think we can agree that userInteractive is wrong, as user interactive is exactly what this code is not. By using a background serial queue you are already saying you don't really care exactly when this code runs. And a timer is already approximate; that is, you are not going to be calling runActivity at exact time intervals either. On the other hand the queue is serial so you won't miss or overlap any calls. Finally, you are going to be networking which means you've thrown away all hope of rapid response, as you are using the totally unpredictable network. Thus .default or .utility, or unspecified, is far more appropriate.

But I would also question your use of a background queue at all. The timer is already effectively running in the background. Moreover, networking itself is already in the background, and your queue won't affect that in any way. I think you should throw your background queue away and let the networking do its usual work in the usual way. The place to worry about what queue you're on is when you get the callback from the network, and that is something you specify in a totally different way.

By the way, your RunLoop.current.add is wrong. Delete it. A scheduled timer is already added to the run loop for you; that is exactly what scheduled means.

2
Rob On

Matt has summarized the concerns well. Timer is asynchronous (non-blocking) and your periodic task would appear to be an asynchronous network request (also non-blocking), so introducing a dispatch to yet another queue is completely unnecessary. It actually introduces a tiny inefficiency, regardless of the queue’s QoS.


But you ask how one specifies a QoS for timer queue. While your example is not one of them, there actually are rare situations where one wants to have a timer run in the background on a queue of a particular QoS. For example, we might do this if the periodic task is slow and synchronous.

In that scenario, we would not use Timer on the main run loop, which then dispatches to some background queue. Instead, we would bypass the main thread entirely. We would simply add a timer to a background queue directly. To do that, rather than a Timer, we would use a DispatchSourceTimer, a GCD-based timer:

class PeriodicService {
    private var timer: DispatchSourceTimer?
    
    func start() {
        let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timer.utility", qos: .utility)
        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.setEventHandler { [weak self] in
            self?.someSlowSynchronousTask()
        }
        timer.schedule(deadline: .now(), repeating: 30)
        timer.resume()
        self.timer = timer
    }
    
    func cancel() {
        timer?.cancel()
        timer = nil
    }
    
    func someSlowSynchronousTask() { … }
}

This avoids any use of the main thread, entirely. Every 30 seconds, it runs that synchronous task on that background queue. Here I am using a .utility QoS. Or, you might use .background QoS. But we consciously avoid .userInteractive (or, worse, .userInitiated) because we never want a background process to take precedence over something for which the user is actually waiting.

We would call start when we want the timer to start. We would call cancel when we want the timer to stop. (And, because it is not using a run loop, unlike Timer, this will actually stop the timer if this DispatchSourceTimer falls out of scope.)

Again, we would not use this to periodically launch a task that is, itself, asynchronous, such as in your example. We would only use this in those rare cases where the task is synchronous and is therefore important to get off the main thread.