1

Very similar issue is already discussed here. The problem at hand and what I am trying to achieve is to call a function on a given object in the thread it is created at. Here is the complete case:

  1. an instance of class A is created in a given NSThread (Thread A) (not the main one). The instance keeps its creating NSThread as a member variable.
  2. an instance of class B has one of its member functions executing in another NSThread - Thread B, and wants to call a function of A in A's creation thread. Thus B's currently executing function issues the following call:

    [_a performSelector: @(fun) 
               onThread: _a.creationThread
             withObject: nil
          waitUntilDone: NO];
    

If the creation thread of A's instance is not the main one, fun never gets called. If the creation thread is the main one it is always called. First I was thinking whether the thread that created A's instance has been destroyed and the pointer points to an invalid thread but actually calling any functions on the thread object (Thread A) produces valid results and no crashes. Also checking the object is valid according to this check. Any suggestions?


Update:

What I'm doing is creating a timer on a background thread:

_timer = [NSTimer timerWithTimeInterval:60.0 target:self selector:@selector(fun:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

This code is the one starting the timer. The timer is not started in any specific background thread. It just happens that the function that creates the timer could be called in any thread. Thus the timer should be invalidated in the exactly same one as the NSTimer documentation states: "you should always call the invalidate method from the same thread on which the timer was installed."

Community
  • 1
  • 1
Ivan Caravanio
  • 597
  • 1
  • 7
  • 20
  • 2
    I wonder if (a) you started the thread; (b) if you have a run loop running; and (c) if you have anything blocking that thread. I'd suggest you edit your question and include a [very small reproducible example](http://stackoverflow.com/help/mcve) of your problem which shows the creation and starting of the thread plus the starting of the run loop. Note, we don't want to see all of your existing code, but rather distill it down to the bare minimum to reproduce your behavior. (As an aside, GCD makes this stuff much easier, but I assume you have some compelling reason to use `NSThread`.) – Rob Jun 25 '16 at 16:43
  • I was about to create a small reproducible example but focused on fixing the problem instead. GCD won't help - there is a timer created in a given thread in object A and it should be invalidated in the same thread. I can't cache the queue that dispatches the call at the moment of the timer creation - `dispatch_get_current_queue()` is deprecated. Also, I know it isn't very healthy to mix up `NSThread`'s, dispatch queues, NSOperations. I don't like the solution of caching either a thread or a dispatch queue but if you have any better suggestions, please share them. – Ivan Caravanio Jun 25 '16 at 16:49
  • 1
    You could use a GCD dispatch timer source. Those can be canceled from any thread. – Ken Thomases Jun 25 '16 at 16:54
  • 2
    First, re [mcve](http://stackoverflow.com/help/mcve), I hear you, but there are tons of places you could have gone wrong, so without seeing what you've done, it's going to be impossible for anyone to help you. Re other approaches, we don't know what you're trying to do so it's impossible to say. But, for example, all you're trying to do is to run a timer on a background thread, then use a GCD dispatch timer source (http://stackoverflow.com/a/25952724/1271826). Please edit the question describing what you're trying to do. – Rob Jun 25 '16 at 16:54
  • `_timer = [NSTimer timerWithTimeInterval:60.0 target:self selector:@selector(fun:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];` This code is the one starting the timer. The timer is not started in any specific background thread. It just happens that the function that creates the timer could be called in any thread. Thus the timer should be invalidated in the exactly same one as the `NSTimer` documentation states: "you should always call the invalidate method from the same thread on which the timer was installed." – Ivan Caravanio Jun 25 '16 at 17:00
  • 1
    There's more to it than just adding the timer to a run loop. You have to keep the run loop alive (and there are a couple of ways of doing that). But, seriously, dispatch timer source is a much, much easier way of running a timer on a background thread. – Rob Jun 25 '16 at 17:33
  • 2
    Note that there is almost never a good reason to call `[performSelector:onThread:...]` in modern ObjC code. It is exceedingly rare that any form of `NSThread` is appropriate for modern ObjC. In almost all cases (minus a few cases where you are tied to a large, existing pthreads codebase), GCD provides much better tools. It is very easy to create problems for yourself with `NSThread`, and while not deprecated, it is highly discouraged. See https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html for more. – Rob Napier Jun 25 '16 at 18:37
  • 1
    The problem is that I am trying to apply a fix to a framework which implementation uses very old code. @Rob, Ken Thomases, many thanks for the proposed solution. – Ivan Caravanio Jun 25 '16 at 19:41

1 Answers1

2

To run timer on background thread, you have two options.

  1. Use dispatch timer source:

    @property (nonatomic, strong) dispatch_source_t timer;
    

    and you can then configure this timer to fire every two seconds:

    - (void)startTimer {
        dispatch_queue_t queue = dispatch_queue_create("com.domain.app.timer", 0);
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(self.timer, ^{
            // call whatever you want here
        });
        dispatch_resume(self.timer);
    }
    
    - (void)stopTimer {
        dispatch_cancel(self.timer);
        self.timer = nil;
    }
    
  2. Run NSTimer on background thread. To do this, you can do something like:

    @property (atomic) BOOL shouldKeepRunning;
    @property (nonatomic, strong) NSThread *timerThread;
    @property (nonatomic, strong) NSTimer *timer;
    

    And

    - (void)startTimerThread {
        self.timerThread = [[NSThread alloc] initWithTarget:self selector:@selector(startTimer:) object:nil];
        [self.timerThread start];
    }
    
    - (void)stopTimerThread {
        [self performSelector:@selector(stopTimer:) onThread:self.timerThread withObject:nil waitUntilDone:false];
    }
    
    - (void)startTimer:(id)__unused object {
        @autoreleasepool {
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES];
            [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
            self.shouldKeepRunning = YES;
            while (self.shouldKeepRunning && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])
                ;
    
            self.timerThread = nil;
        }
    }
    
    - (void)handleTimer:(NSTimer *)timer {
        NSLog(@"tick");
    }
    
    - (void)stopTimer:(id)__unused object {
        [self.timer invalidate];
        self.timer = nil;
        self.shouldKeepRunning = FALSE;
    }
    

    I'm not crazy about the shouldKeepRunning state variable, but if you look at the Apple documentation for the run method, they discourage the reliance upon adding sources/timers to run loops:

    If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

    BOOL shouldKeepRunning = YES;        // global
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];
    while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
    

Personally, I'd recommend the dispatch timer approach.

Rob
  • 415,655
  • 72
  • 787
  • 1,044