1

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?

mfaani
  • 33,269
  • 19
  • 164
  • 293
user784637
  • 15,392
  • 32
  • 93
  • 156

4 Answers4

2

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.

Lutz
  • 1,734
  • 9
  • 18
  • Both are added to runloops, the first is implicitly added to `.default` and the second one is added to `.common`. If you don't add a timer to a runloop it will fire because it's been implicitly added to `.default` – user784637 Jun 22 '20 at 03:20
  • I understand that the timers are added implicitly, hence my answer. What you refer to as `.default`, `.common`, however, are not different `RunLoop` instances. These are modes! – Lutz Jun 22 '20 at 07:57
  • My mistake, I misunderstood, thanks for clarifying. – user784637 Jun 22 '20 at 09:35
2

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.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • re "You must not add the timer to the runloop yourself.", the only reason I did that was because I wanted that logic to run when people are scrolling through the app, so I added it to the `.common` runloop https://www.hackingwithswift.com/articles/117/the-ultimate-guide-to-timer – user784637 Jun 22 '20 at 03:15
  • Please read the linked article carefully. If you need to control the runloop you have to use the *non-scheduled* API (`Timer(timeInterval:...`) – vadian Jun 22 '20 at 04:27
  • Good catch, I missed that. – user784637 Jun 22 '20 at 09:34
  • Nevertheless `DispatchSourceTimer` is more accurate, more reliable and more independent – vadian Jun 22 '20 at 09:40
  • Gotcha - now I'm running into the error `... must be used from main thread only`, is there any way around this using your solution? – user784637 Jun 22 '20 at 10:13
  • `UITableView.indexPathsForVisibleRows must be used from main thread only` and `UIScrollView.contentOffset must be used from main thread only` – user784637 Jun 22 '20 at 10:27
  • 1
    I see, you have to run UI related stuff in the event handlers on the main thread. Please see the edit – vadian Jun 22 '20 at 10:29
  • Thanks so much. To cancel the timer is the best way `timer1.cancel()`? Also - now that the handlers are async, does this affect the tasks not executing at the same time? – user784637 Jun 22 '20 at 12:10
  • 1
    It depends. If you want just to stop a timer once, `cancel` is fine. But if you want to start and stop the timers dynamically declare the properties optional and add a logic to control starting and stopping the timers reliably.. – vadian Jun 22 '20 at 12:16
  • Gotcha thanks. Also I edited the comment after posting it, but now that the handlers are async, does this affect the tasks not executing at the same time? – user784637 Jun 22 '20 at 12:21
  • 1
    The `main` queue is serial, too. – vadian Jun 22 '20 at 12:26
0

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...

nghiahoang
  • 538
  • 4
  • 10
0

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
    }
}

Gereon
  • 17,258
  • 4
  • 42
  • 73