Ensuring the execution logic of two timers do not run at the same time

551 Views Asked by At

I have two timers that run at different time intervals, one of which is added to a runloop.

let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    // do some stuff
}

let timer2 = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
    // do some other stuff
}
RunLoop.current.add(timer2, forMode: RunLoop.Mode.common)

I have a condition which requires the execution logic of the two timers do not run at the same time since they are both modifying the same data structures.

Is this already guaranteed by iOS? If not, what's the best way I can ensure this happens?

4

There are 4 best solutions below

0
nghiahoang On

If you schedule both two timers, nothing ensure they'll fired at the same time.

You could ensure it by, schedule timer2 in fire block of timer1, an so on...

3
Lutz On

As long as you schedule the timers on the same RunLoop, you are safe. A RunLoop is always associated with exactly one Thread and so the blocks will be executed on that Thread. Hence, they will never fire at the exact same time. Things will be different, though, if you schedule them on two different RunLoop instances. I'm not sure what you mean when you say that only one timer is added to a RunLoop. If you don't add a timer to a RunLoop, it won't fire at all.

0
Gereon On

If you need to ensure serial execution of the timer blocks regardless of where the timers themselves were scheduled, you should create your own serial queue and dispatch your timer's work into that:

let timerQueue = DispatchQueue(label: "timerQueue")
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    timerQueue.async {
        // do stuff
    }
}

let timer2 = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
    timerQueue.async {
        // do some other stuff
    }
}

11
vadian On

Your code does not what you describe: The API scheduledTimer(withTimeInterval adds the timer to the runloop implicitly. You must not add the timer to the runloop yourself. So both timers are running on the runloop.


I recommend to use DispatchSourceTimer and a custom DispatchQueue. The queue is serial by default and works as FIFO (first in first out). The serial queue guarantees that the tasks are not executed at the same time.

The timers are suspended after being created. You have to call activate() (or resume() in iOS 9 and lower) to start the timers for example in viewDidLoad

class Controller : UIViewController {
    
    let queue = DispatchQueue(label: "myQueue")
    
    lazy var timer1 : DispatchSourceTimer = {
        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.schedule(deadline:.now() + 1.0, repeating: 1.0)
        timer.setEventHandler(handler: eventHandler1)
        return timer
    }()
    
    
    lazy var timer2 : DispatchSourceTimer = {
        let timer = DispatchSource.makeTimerSource(queue: queue)
        timer.schedule(deadline:.now() + 0.5, repeating: 0.5)
        timer.setEventHandler(handler: eventHandler2)
        return timer
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        timer1.activate()
        timer2.activate()
    }
    
    func eventHandler1() {
        DispatchQueue.main.async {
           // do stuff on the main thread
        }
    }
    
    func eventHandler2() {
        DispatchQueue.main.async {
           // do stuff on the main thread
        }
    }
}

The timer properties are declared lazily to avoid optionals and the event handler closures (() -> Void) are implemented as standard methods.