76

I have a task that needs to be performed every 1 second. Currently I have an NSTimer firing repeatedly every 1 sec. How do I have the timer fire in a background thread (non UI-thread)?

I could have the NSTimer fire on the main thread then use NSBlockOperation to dispatch a background thread, but I'm wondering if there is a more efficient way of doing this.

AWF4vk
  • 5,810
  • 3
  • 37
  • 70

10 Answers10

111

If you need this so timers still run when you scroll your views (or maps), you need to schedule them on different run loop mode. Replace your current timer:

[NSTimer scheduledTimerWithTimeInterval:0.5
                                 target:self
                               selector:@selector(timerFired:)
                               userInfo:nil repeats:YES];

With this one:

NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
                                           target:self
                                         selector:@selector(timerFired:)
                                         userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

For details, check this blog post: Event tracking stops NSTimer

EDIT : second block of code, the NSTimer still runs on the main thread, still on the same run loop as the scrollviews. The difference is the run loop mode. Check the blog post for a clear explanation.

Martin
  • 11,881
  • 6
  • 64
  • 110
Marius
  • 3,589
  • 3
  • 27
  • 30
  • 7
    Correct, this is not a background running task, but to run the timer in a different thread, not to block the UI. – Marius Dec 18 '12 at 07:35
  • 7
    The code makes sense here, but the explanation is wrong. The mainRunLoop runs on the main/UI thread. All you're doing here is configuring it to run from the main thread at different modes. – Steven Fisher Apr 28 '14 at 21:03
  • 2
    I tried this. This is not running in different thread. It is running in main thread. So it block the UI operations. – Anbu Raj May 02 '14 at 06:37
  • 1
    @AnbuRaj it is running on the main thread, but is not blocking the UI, because it's running in a different mode (as Steven Fisher says). Also see http://stackoverflow.com/a/7223765/4712 – Marius May 02 '14 at 11:01
  • I tried this on OSX El Capitan with XCode 7. I got a blocking UI. Note however that I was trying to read named pipes -- perhaps that's the difference. – Volomike Dec 02 '15 at 02:14
  • 1
    For Swift: Create timer: `let timer = NSTimer.init(timeInterval: 0.1, target: self, selector: #selector(AClass.AMethod), userInfo: nil, repeats: true)` Next add the timer: `NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)` – Gerrit Post May 09 '16 at 10:47
  • I, too, feel this doesn't answer the question OP asked, which is about executing NSTimer on a background thread. – VoodooBoot Jan 09 '22 at 17:26
56

If you want to go pure GCD and use a dispatch source, Apple has some sample code for this in their Concurrency Programming Guide:

dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block)
{
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    if (timer)
    {
        dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
        dispatch_source_set_event_handler(timer, block);
        dispatch_resume(timer);
    }
    return timer;
}

Swift 3:

func createDispatchTimer(interval: DispatchTimeInterval,
                         leeway: DispatchTimeInterval,
                         queue: DispatchQueue,
                         block: @escaping ()->()) -> DispatchSourceTimer {
    let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0),
                                               queue: queue)
    timer.scheduleRepeating(deadline: DispatchTime.now(),
                            interval: interval,
                            leeway: leeway)

    // Use DispatchWorkItem for compatibility with iOS 9. Since iOS 10 you can use DispatchSourceHandler
    let workItem = DispatchWorkItem(block: block)
    timer.setEventHandler(handler: workItem)
    timer.resume()
    return timer
}

You could then set up your one-second timer event using code like the following:

dispatch_source_t newTimer = CreateDispatchTimer(1ull * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Repeating task
});

making sure to store and release your timer when done, of course. The above gives you a 1/10th second leeway on the firing of these events, which you could tighten up if you desired.

kelin
  • 11,323
  • 6
  • 67
  • 104
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • I like NSTimer because of its `invalidate` method. Will I get similar behavior using `dispatch_suspend()` or `dispatch_source_cancel()`? Are both required? – Neal Ehardt Nov 22 '13 at 04:41
  • @NealEhardt did you figure this out? I also need invalidate on a background thread. I'm doing concurrent operations on a background thread and they need timeouts if they don't complete. but if they do complete i need to invalidate the timeout timer. – horseshoe7 Apr 04 '14 at 11:45
  • 2
    @horseshoe7 - `dispatch_suspend()` and `dispatch_resume()` will pause and resume a dispatch timer like this. Invalidation before removal is done using `dispatch_source_cancel()` and then `dispatch_release()` (the latter may not be necessary for ARC-enabled applications on certain OS versions). – Brad Larson Apr 04 '14 at 15:46
  • I took the easy way out. This solved my problem admirably! https://github.com/mindsnacks/MSWeakTimer – horseshoe7 Apr 05 '14 at 21:07
  • Doesn't work for me :( on 3rd line says function definition is not allowed here ... – Coldsteel48 May 27 '14 at 01:25
  • If there is a way i can create a repeatable timer with this solution? invoking dispatch_resume() inside dispatch block crushing an app. – Artem Zaytsev Jan 17 '17 at 21:54
  • @ZhouHao even if the app is running in the background? not suspended – Gukki5 Aug 14 '17 at 00:48
22

The timer would need to be installed into a run loop operating on an already-running background thread. That thread would have to continue to run the run loop to have the timer actually fire. And for that background thread to continue being able to fire other timer events, it would need to spawn a new thread to actually handle events anyway (assuming, of course, that the processing you're doing takes a significant amount of time).

For whatever it's worth, I think handling timer events by spawning a new thread using Grand Central Dispatch or NSBlockOperation is a perfectly reasonable use of your main thread.

Steven Fisher
  • 44,462
  • 20
  • 138
  • 192
  • I prefer the NSBlockOperations method too, except it doesn't seem to dispatch to the background thread. It only does concurrent threads for what's added, but only in the current thread. – AWF4vk Nov 29 '11 at 02:22
  • @David: It depends on what queue you add the operation to. If you add it to the main queue, then it will run on the main thread. If you add it to a queue you create yourself, then (at least on the Mac) it will run on another thread. – Peter Hosey Nov 29 '11 at 07:43
18

This should work,

It repeats a method every 1 second in a background queue without using NSTimers :)

- (void)methodToRepeatEveryOneSecond
{
    // Do your thing here

    // Call this method again using GCD 
    dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    double delayInSeconds = 1.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(popTime, q_background, ^(void){
        [self methodToRepeatEveryOneSecond];
    });
}

If you are in the main queue and you want to call above method you could do this so it changes to a background queue before is run :)

dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(q_background, ^{
    [self methodToRepeatEveryOneSecond];
});

Hope it helps

nacho4d
  • 43,720
  • 45
  • 157
  • 240
  • Note, this runs the method on the global background queue, rather than on a specific thread. This may or may not be what the OP wanted. – Lily Ballard Nov 29 '11 at 01:45
  • 4
    Also, this isn't going to be completely accurate. Any delay introduced in the firing of the timer, or the processing of the method, will delay the next callback. It's probably better use a `dispatch_source` timer if you want to use GCD to power this thing. – Lily Ballard Nov 29 '11 at 01:45
  • 2
    @HimanshuMahajan as far as I can tell, it's not really a loop as much as a one-event timer and then it stops. That's why in the `methodToRepeatEveryOneSecond` class method has to restart it again. So, if you wanted to stop it, you'd put a condition above the `dispatch_queue_t ...` line to do a return if you didn't want to continue. – Volomike Dec 02 '15 at 02:20
  • I was using named pipes IPC and needed a background loop to check it. This particular routine works for me, while the one from [Marius](http://stackoverflow.com/a/12173900/105539) didn't work for me under that condition. – Volomike Dec 02 '15 at 02:22
15

For swift 3.0,

Tikhonv's answer does not explain too much. Here adds some of my understanding.

To make things short first, here is the code. It is DIFFERENT from Tikhonv's code at the place where I create the timer. I create the timer using constructer and add it into the loop. I think the scheduleTimer function will add the timer on to the main thread's RunLoop. So it is better to create timer using the constructor.

class RunTimer{
  let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
  let timer: Timer?

  private func startTimer() {
    // schedule timer on background
    queue.async { [unowned self] in
      if let _ = self.timer {
        self.timer?.invalidate()
        self.timer = nil
      }
      let currentRunLoop = RunLoop.current
      self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
      currentRunLoop.add(self.timer!, forMode: .commonModes)
      currentRunLoop.run()
    }
  }

  func timerTriggered() {
    // it will run under queue by default
    debug()
  }

  func debug() {
     // print out the name of current queue
     let name = __dispatch_queue_get_label(nil)
     print(String(cString: name, encoding: .utf8))
  }

  func stopTimer() {
    queue.sync { [unowned self] in
      guard let _ = self.timer else {
        // error, timer already stopped
        return
      }
      self.timer?.invalidate()
      self.timer = nil
    }
  }
}

Create Queue

First, create a queue to make timer run on background and store that queue as a class property in order to reuse it for stop timer. I am not sure if we need to use the same queue for start and stop, the reason I did this is because I saw a warning message here.

The RunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an RunLoop object running in a different thread, as doing so might cause unexpected results.

So I decided to store the queue and use the same queue for the timer to avoid synchronization issues.

Also create an empty timer and stored in the class variable as well. Make it optional so you can stop the timer and set it to nil.

class RunTimer{
  let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
  let timer: Timer?
}

Start Timer

To start timer, first call async from DispatchQueue. Then it is a good practice to first check if the timer has already started. If the timer variable is not nil, then invalidate() it and set it to nil.

The next step is to get the current RunLoop. Because we did this in the block of queue we created, it will get the RunLoop for the background queue we created before.

Create the timer. Here instead of using scheduledTimer, we just call the constructor of timer and pass in whatever property you want for the timer such as timeInterval, target, selector, etc.

Add the created timer to the RunLoop. Run it.

Here is a question about running the RunLoop. According to the documentation here, it says it effectively begins an infinite loop that processes data from the run loop's input sources and timers.

private func startTimer() {
  // schedule timer on background
  queue.async { [unowned self] in
    if let _ = self.timer {
      self.timer?.invalidate()
      self.timer = nil
    }

    let currentRunLoop = RunLoop.current
    self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
    currentRunLoop.add(self.timer!, forMode: .commonModes)
    currentRunLoop.run()
  }
}

Trigger Timer

Implement the function as normal. When that function gets called, it is called under the queue by default.

func timerTriggered() {
  // under queue by default
  debug()
}

func debug() {
  let name = __dispatch_queue_get_label(nil)
  print(String(cString: name, encoding: .utf8))
}

The debug function above is use to print out the name of the queue. If you ever worry if it has been running on the queue, you can call it to check.

Stop Timer

Stop timer is easy, call validate() and set the timer variable stored inside class to nil.

Here I am running it under the queue again. Because of the warning here I decided to run all the timer related code under the queue to avoid conflicts.

func stopTimer() {
  queue.sync { [unowned self] in
    guard let _ = self.timer else {
      // error, timer already stopped
      return
    }
    self.timer?.invalidate()
    self.timer = nil
  }
}

Questions related to RunLoop

I am somehow a little bit confused on if we need to manually stop the RunLoop or not. According to the documentation here, it seems that when no timers attached to it, then it will exits immediately. So when we stop the timer, it should exists itself. However, at the end of that document, it also said:

removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

I tried the solution below that provided in the documentation for a guarantee to terminate the loop. However, the timer does not fire after I change .run() to the code below.

while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};

What I am thinking is that it might be safe for just using .run() on iOS. Because the documentation states that macOS is install and remove additional input sources as needed to process requests targeted at the receiver's thread. So iOS might be fine.

nuynait
  • 1,912
  • 20
  • 27
  • 3
    Keep in mind that queues are not threads and two queues may be actually run on the same thread (even on the main thread). This may cause unexpected problems, considering that `Timer` is bound to a `RunLoop` which is bound to a thread, not to a queue. – kelin Jul 12 '17 at 09:05
7

Today after 6 years, I try to do same thing, here is alternative soltion: GCD or NSThread.

Timers work in conjunction with run loops, a thread's runloop can be get from the thread only, so the key is that schedule timer in the thread.

Except main thread's runloop, runloop should start manually; there should be some events to handle in running runloop, like Timer, otherwise runloop will exit, and we can use this to exit a runloop if timer is the only event source: invalidate the timer.

The following code is Swift 4:

Solution 0: GCD

weak var weakTimer: Timer?
@objc func timerMethod() {
    // vefiry whether timer is fired in background thread
    NSLog("It's called from main thread: \(Thread.isMainThread)")
}

func scheduleTimerInBackgroundThread(){
    DispatchQueue.global().async(execute: {
        //This method schedules timer to current runloop.
        self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
        //start runloop manually, otherwise timer won't fire
        //add timer before run, otherwise runloop find there's nothing to do and exit directly.
        RunLoop.current.run()
    })
}

Timer has strong reference to target, and runloop has strong reference to timer, after timer invalidate, it release target, so keep weak reference to it in target and invalidate it in appropriate time to exit runloop(and then exit thread).

Note: as an optimization, syncfunction of DispatchQueue invokes the block on the current thread when possible. Actually, you execute above code in main thread, Timer is fired in main thread, so don't use sync function, otherwise timer is not fired at the thread you want.

You could name thread to track its activity by pausing program executing in Xcode. In GCD, use:

Thread.current.name = "ThreadWithTimer"

Solution 1: Thread

We could use NSThread directly. Don't afraid, code is easy.

func configurateTimerInBackgroundThread(){
    // Don't worry, thread won't be recycled after this method return.
    // Of course, it must be started.
    let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
    thread.start()
}

@objc func addTimer() {
    weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
    RunLoop.current.run()
}

Solution 2: Subclass Thread

If you want to use Thread subclass:

class TimerThread: Thread {
    var timer: Timer
    init(timer: Timer) {
        self.timer = timer
        super.init()
    }

    override func main() {
        RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }
}

Note: don't add timer in init, otherwise, timer is add to init's caller's thread's runloop, not this thread's runloop, e.g., you run following code in main thread, if TimerThread add timer in init method, timer will be scheduled to main thread's runloop, not timerThread's runloop. You can verify it in timerMethod() log.

let timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
weakTimer = timer
let timerThread = TimerThread.init(timer: timer)
timerThread.start()

P.S About Runloop.current.run(), its document suggest don't call this method if we want runloop to terminate, use run(mode: RunLoopMode, before limitDate: Date), actually run() repeatedly invoke this method in the NSDefaultRunloopMode, what's mode? More details in runloop and thread.

seedante
  • 300
  • 4
  • 10
3

My Swift 3.0 solution for iOS 10+, timerMethod() will be called in background queue.

class ViewController: UIViewController {

    var timer: Timer!
    let queue = DispatchQueue(label: "Timer DispatchQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)

    override func viewDidLoad() {
        super.viewDidLoad()

        queue.async { [unowned self] in
            let currentRunLoop = RunLoop.current
            let timeInterval = 1.0
            self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerMethod), userInfo: nil, repeats: true)
            self.timer.tolerance = timeInterval * 0.1
            currentRunLoop.add(self.timer, forMode: .commonModes)
            currentRunLoop.run()
        }
    }

    func timerMethod() {
        print("code")
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        queue.sync {
            timer.invalidate()
        }
    }
}
Tikhonov Aleksandr
  • 13,945
  • 6
  • 39
  • 53
2

Swift only (although can probably be modified to use with Objective-C)

Check out DispatchTimer from https://github.com/arkdan/ARKExtensions, which "Executes a closure on specified dispatch queue, with specified time intervals, for specified number of times (optionally). "

let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
    // body to execute until cancelled by timer.cancel()
}
user1244109
  • 2,166
  • 2
  • 26
  • 24
0
class BgLoop:Operation{
    func main(){
        while (!isCancelled) {
            sample();
            Thread.sleep(forTimeInterval: 1);
        }
    }
}
john07
  • 562
  • 6
  • 16
-2

If you want your NSTimer to run in even background, do the following-

  1. call [self beginBackgroundTask] method in applicationWillResignActive methods
  2. call [self endBackgroundTask] method in applicationWillEnterForeground

That's it

-(void)beginBackgroundTask
{
    bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [self endBackgroundTask];
    }];
}

-(void)endBackgroundTask
{
    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}
Vishwas Singh
  • 1,497
  • 1
  • 18
  • 16
  • 1
    I think you misunderstood the question. He wants to run the time on a background thread but not necessarily on app's background state. Also, the question is about macOS because of the Cocoa tag. – Emma Labbé Feb 13 '19 at 13:07