22

I was wondering why when you create a repeating timer in a GCD block it doesen't work?

This works fine:

-(void)viewDidLoad{
    [super viewDidLoad];
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
}
-(void)runTimer{
    NSLog(@"hi");
}

But this doesent work:

dispatch_queue_t myQueue;

-(void)viewDidLoad{
    [super viewDidLoad];

    myQueue = dispatch_queue_create("someDescription", NULL);
    dispatch_async(myQueue, ^{
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
    });
}
-(void)runTimer{
    NSLog(@"hi");
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
Praxder
  • 2,315
  • 4
  • 32
  • 51

3 Answers3

64

NSTimers are scheduled on the current thread's run loop. However, GCD dispatch threads don't have run loops, so scheduling timers in a GCD block isn't going to do anything.

There's three reasonable alternatives:

  1. Figure out what run loop you want to schedule the timer on, and explicitly do so. Use +[NSTimer timerWithTimeInterval:target:selector:userInfo:repeats:] to create the timer and then -[NSRunLoop addTimer:forMode:] to actually schedule it on the run loop you want to use. This requires having a handle on the run loop in question, but you may just use +[NSRunLoop mainRunLoop] if you want to do it on the main thread.
  2. Switch over to using a timer-based dispatch source. This implements a timer in a GCD-aware mechanism, that will run a block at the interval you want on the queue of your choice.
  3. Explicitly dispatch_async() back to the main queue before creating the timer. This is equivalent to option #1 using the main run loop (since it will also create the timer on the main thread).

Of course, the real question here is, why are you creating a timer from a GCD queue to begin with?

Community
  • 1
  • 1
Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • another alternative following Kevin's 2nd answer, I solved my issues by replacing the NSTimer with MSWeakTimer ( github.com/mindsnacks/MSWeakTimer ), and just passed the dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0) as the dispatchQueue . – Vivek Gani Mar 16 '14 at 18:09
  • Great summary. Super Thanks. If I understand correctly: 1. Are you saying no GCD dispatch thread has a run loop **except** for main thread? 2. If true then how about dispatch_after? How would it process without a runloop? Or is that it is also implemented in a GCD aware mechanism? – mfaani Aug 04 '17 at 14:33
  • @Honey Dispatch threads don't have run loops. The main thread isn't a dispatch thread (well, unless you're using `dispatch_main()`, and if so, it doesn't have a runloop either, but you're presumably not doing that). The main thread's runloop integrates with GCD to explicitly drain the main queue as part of the runloop's normal operation. `dispatch_after` is part of GCD (hence the word "dispatch" in the name), it doesn't have anything to do with runloops. – Lily Ballard Aug 05 '17 at 18:50
  • Thank you. That was too dense for me to comprehend :( . By your third option I thought you meant do something like `DispatchQueue.main.async{//timer}` But then you said "The main thread isn't a dispatch thread (well, unless you're using dispatch_main(), and if so, it doesn't have a runloop either, but you're presumably not doing that). " FYI I'm asking all this because I'm doing `DispatchQueue.main.async`{//runTimer} *from* a didUpdateLocation callback where my app has Location backgroundModes enabled; My timer could be called either in when app is in background for foreground. 1/2 – mfaani Aug 05 '17 at 19:04
  • (I think my main issue is not understanding the difference of main thread and dispatch thread. Can you elaborate on that?). FYI the timer works, but I'm not 100% if it should/would work all the time. I'm here to 1. know **why** it would work or not 2. Find solutions for it to work. 2/2 – mfaani Aug 05 '17 at 19:05
  • 1
    The main thread is the only thread that exists when the process begins. When an iOS app calls `UIApplicationMain` (which it's doing behind the scenes if you have an application delegate set up in the normal way), it starts the "main run loop" on this thread. The main run loop runs until the application terminates. The main run loop is responsible for handling many things, including all UI events, and most of your code (in particular all your UI code) is running on the main thread, driven by the main run loop. – Lily Ballard Aug 05 '17 at 19:16
  • 1
    A dispatch thread is a thread managed internally by GCD, that it uses to process blocks submitted to dispatch queues. Blocks submitted to the main queue are special in that the main run loop handles them itself, but blocks submitted to other queues are handled by GCD and executed on one of the dispatch threads. They're basically just worker threads. – Lily Ballard Aug 05 '17 at 19:17
  • Hmmm. I can understand a more now. Thanks. In your initial [comment](https://stackoverflow.com/questions/10522928/run-repeating-nstimer-with-gcd/32299190?noredirect=1#comment78010739_10522969) you said: "The main thread...doesn't have a run loop either" but then in your other [comment](https://stackoverflow.com/questions/10522928/run-repeating-nstimer-with-gcd/32299190?noredirect=1#comment78011160_10522969) you said: " The main thread....starts the main run loop on this thread"... so confused as to whether main thread always has a run loop attached to it or not? – mfaani Aug 05 '17 at 19:32
  • @Honey If you use `dispatch_main()` then the main thread doesn't have a runloop, because you're using GCD to drive it. But you can ignore that, because you're not using `dispatch_main()` (it's typically only something that command-line macOS tools might use). – Lily Ballard Aug 05 '17 at 19:34
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151172/discussion-between-honey-and-kevin-ballard). – mfaani Aug 06 '17 at 02:47
  • @KevinBallard It looks `DispatchQueue.main` is a serial queue which run tasks on the main thread. `The main dispatch queue is a globally available serial queue that executes tasks on the application's main thread. This queue works with the application's run loop (if one is present) to interleave the execution of queued tasks with the execution of other event sources attached to the run loop.` – Ryan Aug 15 '17 at 00:08
0

NSTimer is scheduled to thread’s runloop. In code of question, runloop of thread dispatched by GCD is not running. You must start it manually and there must be a way to exit run loop, so you should keep a reference to the NSTimer, and invalidate it in appropriate time.

NSTimer has strong reference to the target, so target can't has strong reference to timer, and runloop has strong reference to the timer.

weak var weakTimer: Timer?
func configurateTimerInBackgroundThread(){
    DispatchQueue.global().async {
        // Pause program execution in Xcode, you will find thread with this name
        Thread.current.name = "BackgroundThreadWithTimer"
        // This timer is scheduled to current run loop
        self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
        // Start current runloop manually, otherwise NSTimer won't fire.
        RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
    }
}

@objc func runTimer(){
    NSLog("Timer is running in mainThread: \(Thread.isMainThread)")
}

If timer is invalidated in future, pause program execution again in Xcode, you will find that thread is gone.

Of course, threads dispatched by GCD have runloop. GCD generate and reuse threads internally, there threads are anonymous to caller. If you don't feel safe to it, you could use Thread. Don't afraid, code is very easy.

Actually, I try same thing last week and get same fail with asker, then I found this page. I try NSThread before I give up. It works. So why NSTimer in GCD can't work? It should be. Read runloop's document to know how NSTimer works.

Use NSThread to work with NSTimer:

func configurateTimerInBackgroundThread(){
    let thread = Thread.init(target: self, selector: #selector(addTimerInBackground), object: nil)
    thread.name = "BackgroundThreadWithTimer"
    thread.start()
}

@objc func addTimerInBackground() {
    self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
    RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
}
seedante
  • 300
  • 4
  • 10
-1

This is a bad Idea. I was about to delete this answer, but I left it here to avoid others from doing the same mistake I did. Thank you #Kevin_Ballard for pointing at this.

You'd only add one line to your example and it'd work just as you wrote it:

[[NSRunLoop currentRunLoop] run]

so you'd get:

    -(void)viewDidLoad{
        [super viewDidLoad];

        myQueue = dispatch_queue_create("someDescription", NULL);
        dispatch_async(myQueue, ^{
            [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] run]
        });
    }

Since your queue myQueue contains a NSThread and it contains a NSRunLoop and since the code in the dispatch_async is run the the context of that NSThread, currentRunLoop would return a stopped run loop associated with the thread of your queue.

Skulas
  • 457
  • 6
  • 6
  • 1
    This is a bad idea. You've now coopted a dispatch thread to use as a runloop, and you're never going to return control of the thread back to GCD. This is basically like putting an infinite loop into a dispatch block. Taking over dispatch threads and never returning control of them to GCD is going to hurt the performance of the whole system, and if you do that to enough threads you can actually make GCD stop processing blocks submitted to non-global queues entirely. – Lily Ballard Aug 05 '17 at 19:01
  • You code just need a way to exit runloop. Keep a reference to NSTimer and invalidate it in appropriate time. – seedante Jan 22 '18 at 12:36